mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
616afdc9ea | ||
|
|
2f77ca1187 | ||
|
|
bda99f1d77 | ||
|
|
bb76ea6412 | ||
|
|
a8151e9c17 | ||
|
|
6cd06848b5 | ||
|
|
0e3d5f7036 | ||
|
|
5cb53c0573 | ||
|
|
7f2ebabc46 | ||
|
|
f640774589 | ||
|
|
b832d3aecc | ||
|
|
2809bdc201 | ||
|
|
31579ed237 | ||
|
|
b7159e0979 | ||
|
|
a92d900552 | ||
|
|
29ed12e7fd | ||
|
|
0ef6494ad2 | ||
|
|
ec7b6b4285 | ||
|
|
4f6f7410f2 | ||
|
|
ea34992f03 | ||
|
|
9d59389fc0 | ||
|
|
1d26ee66ea | ||
|
|
642d6872d1 | ||
|
|
940a1aabd9 | ||
|
|
8a8faa83e4 | ||
|
|
14d7e75faf | ||
|
|
4f27ae4011 | ||
|
|
c9f2958fe4 | ||
|
|
f9f8456ee9 | ||
|
|
7246b7e752 | ||
|
|
329e908e84 | ||
|
|
d795facb29 | ||
|
|
c5f29f85ce | ||
|
|
af119267cc | ||
|
|
9e367e1d42 | ||
|
|
cb747236d9 | ||
|
|
b73bd91783 | ||
|
|
21632bf044 | ||
|
|
a836169da4 | ||
|
|
ce93a9a2db | ||
|
|
52407bd8d8 | ||
|
|
9ef079efab | ||
|
|
afd2bd3bad | ||
|
|
022b011a90 | ||
|
|
7a1494cbe4 | ||
|
|
2b98c0b250 | ||
|
|
f10504e95f | ||
|
|
7fbe649dc6 | ||
|
|
11e7232bd6 | ||
|
|
9fc27e74f8 | ||
|
|
1572c10384 | ||
|
|
553f3c2ee0 | ||
|
|
837733b4c2 | ||
|
|
3468628f1f | ||
|
|
eee646b636 | ||
|
|
e78318c4cb | ||
|
|
8ccab3ab80 | ||
|
|
e2ecd14d86 | ||
|
|
6f8131a628 | ||
|
|
5761ca12b6 | ||
|
|
bfe181ac1f | ||
|
|
f5632c27f4 | ||
|
|
6bec0b402c | ||
|
|
b0262bf8f1 | ||
|
|
c57822b3b3 | ||
|
|
c313bbd83d | ||
|
|
b9e8bb70eb | ||
|
|
62da550c6c | ||
|
|
3d9fb17d17 | ||
|
|
2fd7aa6de7 | ||
|
|
b78dd8d542 | ||
|
|
1df51621fa | ||
|
|
33703d15e2 | ||
|
|
b682c7a281 | ||
|
|
d393ea233e | ||
|
|
563e3a2791 | ||
|
|
651f2b8f5d | ||
|
|
077bcba377 | ||
|
|
9084df1e5c | ||
|
|
2bb6d7f49d | ||
|
|
83cf13a483 | ||
|
|
1b22a88b80 | ||
|
|
252c1a7236 | ||
|
|
7f371f6915 | ||
|
|
1ccd48efba | ||
|
|
a4c077c388 | ||
|
|
2b1e85c86c | ||
|
|
a68f119afe | ||
|
|
9ebf7d668f | ||
|
|
4570c481ef | ||
|
|
5d56629d10 | ||
|
|
37c7100c8b | ||
|
|
35266bda0e | ||
|
|
6a90a43995 | ||
|
|
096262b6f1 | ||
|
|
03df636e5e | ||
|
|
02bc9de560 | ||
|
|
8fe0a2450e | ||
|
|
d8bb99d547 | ||
|
|
79f9ce437a | ||
|
|
6cb02818ef | ||
|
|
e94fb2dff2 | ||
|
|
14c39b1f99 | ||
|
|
6dafa5954e | ||
|
|
da1dd21a9e | ||
|
|
011036b87f | ||
|
|
35ef2edb3f | ||
|
|
ebcf866891 | ||
|
|
60c6d87b05 | ||
|
|
2aa38fe247 | ||
|
|
d19faa1627 | ||
|
|
e910f299b9 | ||
|
|
2ebf26854e | ||
|
|
7ff067937a | ||
|
|
2727b74cc7 | ||
|
|
3bb9c5f9a7 | ||
|
|
5623590c77 | ||
|
|
29050913b3 | ||
|
|
aead2bf783 | ||
|
|
2aaae70372 | ||
|
|
de29a258c6 | ||
|
|
8ca5b480e4 | ||
|
|
f6ec785b0d | ||
|
|
4834c0cb4b | ||
|
|
1f0016c616 | ||
|
|
74e4be139f | ||
|
|
b584336b62 | ||
|
|
20b0d7a298 | ||
|
|
5741f24aa9 | ||
|
|
b5dfc9fc75 | ||
|
|
4318e95618 | ||
|
|
113cbb6eb8 | ||
|
|
3ebed060b7 | ||
|
|
270ef0db47 | ||
|
|
0b306b436a | ||
|
|
c5519bc557 | ||
|
|
23d6f2ae75 | ||
|
|
9da7c54ae9 | ||
|
|
928cb3aa9b | ||
|
|
3e6815d73f | ||
|
|
cb08f56066 | ||
|
|
e3f486a063 | ||
|
|
0a1e25e16a | ||
|
|
ff4966ad3f | ||
|
|
0cb10be2d5 | ||
|
|
901863989b | ||
|
|
99377764c1 | ||
|
|
426d70fbcf | ||
|
|
f315722b31 | ||
|
|
73afe8e742 | ||
|
|
db67b8a857 | ||
|
|
e05475d58a | ||
|
|
dceee8f50e | ||
|
|
b893252ad1 | ||
|
|
0755a2b657 | ||
|
|
90bf8b0b2e | ||
|
|
5ff28c751d | ||
|
|
7b08edb5a4 | ||
|
|
fbadcbeb29 | ||
|
|
e991375f55 | ||
|
|
33c99014ae | ||
|
|
fbd6ff6ead | ||
|
|
c4ab2eb3b5 | ||
|
|
44b6df0ce5 | ||
|
|
14a42505a9 | ||
|
|
44cbd33cb7 | ||
|
|
4411a71d06 | ||
|
|
bfe37bc668 | ||
|
|
d784e0a52b | ||
|
|
d73a2942a2 | ||
|
|
8af4847373 | ||
|
|
44f7367e21 | ||
|
|
0733b0d521 | ||
|
|
771a9eebcf | ||
|
|
0fdede5d7a | ||
|
|
56b2130c6e | ||
|
|
6c018b94da | ||
|
|
63f4598737 | ||
|
|
598cdf0a21 | ||
|
|
eea7bfc6bf | ||
|
|
8521c96e8a | ||
|
|
d90cc5142b | ||
|
|
37aab65396 | ||
|
|
baff003ea8 | ||
|
|
03427e35a7 | ||
|
|
170fa40014 | ||
|
|
330b30d5d2 | ||
|
|
67b8274faf | ||
|
|
02ee3fb974 | ||
|
|
93ced1956c | ||
|
|
a9e358ea57 | ||
|
|
ffcdbc5d0c | ||
|
|
6740cae10f | ||
|
|
915379459d | ||
|
|
a4a12bae27 | ||
|
|
94fc356338 | ||
|
|
08ada24a53 | ||
|
|
b59846b9fa | ||
|
|
c9ec463893 | ||
|
|
38ce150f80 | ||
|
|
408b538a45 | ||
|
|
1e82465559 | ||
|
|
85ac60e2c3 | ||
|
|
aaba87ac57 | ||
|
|
d6403ace6e | ||
|
|
6c989de994 | ||
|
|
4908f1a8ec | ||
|
|
f3c6c1172e | ||
|
|
9658a34605 | ||
|
|
5d153e05ef | ||
|
|
2ba77e015c | ||
|
|
9d958033a5 | ||
|
|
4adad5d98a | ||
|
|
b9737101cd | ||
|
|
9f27e28a45 | ||
|
|
9a7b2f1d0d | ||
|
|
0df33de73e | ||
|
|
cda2edf92c | ||
|
|
d38f59c18c | ||
|
|
4a7150204c | ||
|
|
bfb8e5df82 | ||
|
|
376d7cde12 | ||
|
|
a466df9c52 | ||
|
|
117438cff0 | ||
|
|
c6483751b5 | ||
|
|
2bf0bb5fb3 | ||
|
|
e9bc7c7163 | ||
|
|
0a0d0f2bdf | ||
|
|
3293b77f18 | ||
|
|
69aeb2d86f | ||
|
|
7298c1f49a | ||
|
|
ba03580659 | ||
|
|
a93f07c651 | ||
|
|
c0edfb46bb | ||
|
|
6c5a0c6df1 | ||
|
|
80d64e7b64 | ||
|
|
3e7362200a | ||
|
|
c400744040 | ||
|
|
91552d5fd2 | ||
|
|
6100d9b4df | ||
|
|
37c1540ff4 | ||
|
|
3239701ba4 | ||
|
|
81408bb853 | ||
|
|
6414301936 | ||
|
|
db513d1b27 | ||
|
|
0ecdf2ccbd | ||
|
|
c05e20cf1e | ||
|
|
f4334d7307 | ||
|
|
201920d161 | ||
|
|
e809ee19ee | ||
|
|
4684f36c38 | ||
|
|
bb3ddf1961 | ||
|
|
0e1f0fd730 | ||
|
|
7b1d4770e9 | ||
|
|
befa690d9e | ||
|
|
bcf2a17257 | ||
|
|
8c6b28cd81 | ||
|
|
b916f95d4d | ||
|
|
42d94dd44b | ||
|
|
f5fd5fdf5b | ||
|
|
e85554827f | ||
|
|
5d32937321 | ||
|
|
8de9376a10 | ||
|
|
35a78dbc4e | ||
|
|
2e69113688 | ||
|
|
eb97f8ee75 | ||
|
|
ea4aac6af1 | ||
|
|
a1897fd3b1 | ||
|
|
ea889ce2ad | ||
|
|
c20553ce68 | ||
|
|
9732107ba6 | ||
|
|
b22c4fb65a | ||
|
|
4b638a9608 | ||
|
|
b0dbccaf3c | ||
|
|
36683e1dd7 | ||
|
|
15a0be6107 | ||
|
|
a728cb61d2 | ||
|
|
606e1cd4da | ||
|
|
c7ef362d7a | ||
|
|
d8ccebce60 | ||
|
|
a7c06b2ec4 | ||
|
|
4f7ab6733d | ||
|
|
7714893294 | ||
|
|
d921ff1f68 | ||
|
|
3ca4c48b00 | ||
|
|
cd64bf8324 | ||
|
|
2031787506 | ||
|
|
d43998facf | ||
|
|
acab276d51 | ||
|
|
95040da881 | ||
|
|
ccb25eab36 | ||
|
|
fb2f3ef2b5 | ||
|
|
b2e13d3c03 | ||
|
|
b10e86a4ba | ||
|
|
04611d833d | ||
|
|
2c25719da5 | ||
|
|
3c7514f653 | ||
|
|
42e8279c75 | ||
|
|
5ba60e6f66 | ||
|
|
3fa229b7b3 | ||
|
|
52f7f0212b | ||
|
|
4d8dcad190 | ||
|
|
d7ad3e32d4 | ||
|
|
c9524e9c09 | ||
|
|
c2122acfba | ||
|
|
69335a8bac | ||
|
|
86b39733e1 | ||
|
|
94b23d62db | ||
|
|
942255923b | ||
|
|
68b45c2812 | ||
|
|
77d652da9a | ||
|
|
c7268c7fc4 | ||
|
|
5ffe97f874 | ||
|
|
ec7e4916e5 | ||
|
|
ec55161cb1 | ||
|
|
7559e912f1 | ||
|
|
92bc17463a | ||
|
|
fc4b5225d2 | ||
|
|
3c21079afa | ||
|
|
9792188b0e | ||
|
|
fd7871ae7d | ||
|
|
b32adcce78 | ||
|
|
a6490fa60e | ||
|
|
4fb3d09ee9 | ||
|
|
98db972a6c | ||
|
|
233d1afc29 | ||
|
|
7d0e5e814e | ||
|
|
42dd19c387 | ||
|
|
8e10e1e590 | ||
|
|
c645b7d732 | ||
|
|
010b54f0af | ||
|
|
7cdfdc1bfb | ||
|
|
1b3ff44bc2 | ||
|
|
7dd1f8a6bd | ||
|
|
05105ae2ad | ||
|
|
f913e9e40d | ||
|
|
a2f36f426a | ||
|
|
ccc9e4dab2 | ||
|
|
9203d5c371 | ||
|
|
5b97224836 | ||
|
|
430c6d0c1a | ||
|
|
346c2f55ff | ||
|
|
128991c3dc | ||
|
|
84342fce4e | ||
|
|
d03e723fe0 | ||
|
|
2f59d44f9e | ||
|
|
e4c12fa4e5 | ||
|
|
7f6db5c4d9 | ||
|
|
a72f9f53a4 | ||
|
|
1847b3ead7 | ||
|
|
05b0bdbe0d | ||
|
|
2511932158 | ||
|
|
a19756ee77 | ||
|
|
0786e93bb9 | ||
|
|
c43d7c8725 | ||
|
|
1f3a402b23 | ||
|
|
f404031d01 | ||
|
|
86c2878d43 |
202
Apache-2.0.txt
Normal file
202
Apache-2.0.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
885
CHANGELOG.md
Normal file
885
CHANGELOG.md
Normal file
@@ -0,0 +1,885 @@
|
||||
# Community Crypto Release Notes
|
||||
|
||||
**Topics**
|
||||
|
||||
- <a href="#v1-9-26">v1\.9\.26</a>
|
||||
- <a href="#release-summary">Release Summary</a>
|
||||
- <a href="#major-changes">Major Changes</a>
|
||||
- <a href="#v1-9-25">v1\.9\.25</a>
|
||||
- <a href="#release-summary-1">Release Summary</a>
|
||||
- <a href="#bugfixes">Bugfixes</a>
|
||||
- <a href="#v1-9-24">v1\.9\.24</a>
|
||||
- <a href="#release-summary-2">Release Summary</a>
|
||||
- <a href="#bugfixes-1">Bugfixes</a>
|
||||
- <a href="#v1-9-23">v1\.9\.23</a>
|
||||
- <a href="#release-summary-3">Release Summary</a>
|
||||
- <a href="#bugfixes-2">Bugfixes</a>
|
||||
- <a href="#v1-9-22">v1\.9\.22</a>
|
||||
- <a href="#release-summary-4">Release Summary</a>
|
||||
- <a href="#bugfixes-3">Bugfixes</a>
|
||||
- <a href="#v1-9-21">v1\.9\.21</a>
|
||||
- <a href="#release-summary-5">Release Summary</a>
|
||||
- <a href="#bugfixes-4">Bugfixes</a>
|
||||
- <a href="#v1-9-20">v1\.9\.20</a>
|
||||
- <a href="#release-summary-6">Release Summary</a>
|
||||
- <a href="#bugfixes-5">Bugfixes</a>
|
||||
- <a href="#v1-9-19">v1\.9\.19</a>
|
||||
- <a href="#release-summary-7">Release Summary</a>
|
||||
- <a href="#bugfixes-6">Bugfixes</a>
|
||||
- <a href="#v1-9-18">v1\.9\.18</a>
|
||||
- <a href="#release-summary-8">Release Summary</a>
|
||||
- <a href="#bugfixes-7">Bugfixes</a>
|
||||
- <a href="#v1-9-17">v1\.9\.17</a>
|
||||
- <a href="#release-summary-9">Release Summary</a>
|
||||
- <a href="#bugfixes-8">Bugfixes</a>
|
||||
- <a href="#v1-9-16">v1\.9\.16</a>
|
||||
- <a href="#release-summary-10">Release Summary</a>
|
||||
- <a href="#bugfixes-9">Bugfixes</a>
|
||||
- <a href="#v1-9-15">v1\.9\.15</a>
|
||||
- <a href="#release-summary-11">Release Summary</a>
|
||||
- <a href="#bugfixes-10">Bugfixes</a>
|
||||
- <a href="#v1-9-14">v1\.9\.14</a>
|
||||
- <a href="#release-summary-12">Release Summary</a>
|
||||
- <a href="#bugfixes-11">Bugfixes</a>
|
||||
- <a href="#v1-9-13">v1\.9\.13</a>
|
||||
- <a href="#release-summary-13">Release Summary</a>
|
||||
- <a href="#bugfixes-12">Bugfixes</a>
|
||||
- <a href="#v1-9-12">v1\.9\.12</a>
|
||||
- <a href="#release-summary-14">Release Summary</a>
|
||||
- <a href="#bugfixes-13">Bugfixes</a>
|
||||
- <a href="#known-issues">Known Issues</a>
|
||||
- <a href="#v1-9-11">v1\.9\.11</a>
|
||||
- <a href="#release-summary-15">Release Summary</a>
|
||||
- <a href="#bugfixes-14">Bugfixes</a>
|
||||
- <a href="#v1-9-10">v1\.9\.10</a>
|
||||
- <a href="#release-summary-16">Release Summary</a>
|
||||
- <a href="#bugfixes-15">Bugfixes</a>
|
||||
- <a href="#v1-9-9">v1\.9\.9</a>
|
||||
- <a href="#bugfixes-16">Bugfixes</a>
|
||||
- <a href="#v1-9-8">v1\.9\.8</a>
|
||||
- <a href="#release-summary-17">Release Summary</a>
|
||||
- <a href="#v1-9-7">v1\.9\.7</a>
|
||||
- <a href="#release-summary-18">Release Summary</a>
|
||||
- <a href="#minor-changes">Minor Changes</a>
|
||||
- <a href="#bugfixes-17">Bugfixes</a>
|
||||
- <a href="#v1-9-6">v1\.9\.6</a>
|
||||
- <a href="#release-summary-19">Release Summary</a>
|
||||
- <a href="#bugfixes-18">Bugfixes</a>
|
||||
- <a href="#v1-9-5">v1\.9\.5</a>
|
||||
- <a href="#release-summary-20">Release Summary</a>
|
||||
- <a href="#bugfixes-19">Bugfixes</a>
|
||||
- <a href="#v1-9-4">v1\.9\.4</a>
|
||||
- <a href="#release-summary-21">Release Summary</a>
|
||||
- <a href="#bugfixes-20">Bugfixes</a>
|
||||
- <a href="#v1-9-3">v1\.9\.3</a>
|
||||
- <a href="#release-summary-22">Release Summary</a>
|
||||
- <a href="#bugfixes-21">Bugfixes</a>
|
||||
- <a href="#v1-9-2">v1\.9\.2</a>
|
||||
- <a href="#release-summary-23">Release Summary</a>
|
||||
- <a href="#v1-9-1">v1\.9\.1</a>
|
||||
- <a href="#release-summary-24">Release Summary</a>
|
||||
- <a href="#v1-9-0">v1\.9\.0</a>
|
||||
- <a href="#release-summary-25">Release Summary</a>
|
||||
- <a href="#minor-changes-1">Minor Changes</a>
|
||||
- <a href="#bugfixes-22">Bugfixes</a>
|
||||
- <a href="#v1-8-0">v1\.8\.0</a>
|
||||
- <a href="#release-summary-26">Release Summary</a>
|
||||
- <a href="#minor-changes-2">Minor Changes</a>
|
||||
- <a href="#bugfixes-23">Bugfixes</a>
|
||||
- <a href="#v1-7-1">v1\.7\.1</a>
|
||||
- <a href="#release-summary-27">Release Summary</a>
|
||||
- <a href="#bugfixes-24">Bugfixes</a>
|
||||
- <a href="#v1-7-0">v1\.7\.0</a>
|
||||
- <a href="#release-summary-28">Release Summary</a>
|
||||
- <a href="#minor-changes-3">Minor Changes</a>
|
||||
- <a href="#bugfixes-25">Bugfixes</a>
|
||||
- <a href="#new-modules">New Modules</a>
|
||||
- <a href="#v1-6-2">v1\.6\.2</a>
|
||||
- <a href="#release-summary-29">Release Summary</a>
|
||||
- <a href="#bugfixes-26">Bugfixes</a>
|
||||
- <a href="#v1-6-1">v1\.6\.1</a>
|
||||
- <a href="#release-summary-30">Release Summary</a>
|
||||
- <a href="#bugfixes-27">Bugfixes</a>
|
||||
- <a href="#v1-6-0">v1\.6\.0</a>
|
||||
- <a href="#release-summary-31">Release Summary</a>
|
||||
- <a href="#minor-changes-4">Minor Changes</a>
|
||||
- <a href="#deprecated-features">Deprecated Features</a>
|
||||
- <a href="#bugfixes-28">Bugfixes</a>
|
||||
- <a href="#v1-5-0">v1\.5\.0</a>
|
||||
- <a href="#release-summary-32">Release Summary</a>
|
||||
- <a href="#minor-changes-5">Minor Changes</a>
|
||||
- <a href="#deprecated-features-1">Deprecated Features</a>
|
||||
- <a href="#bugfixes-29">Bugfixes</a>
|
||||
- <a href="#v1-4-0">v1\.4\.0</a>
|
||||
- <a href="#release-summary-33">Release Summary</a>
|
||||
- <a href="#minor-changes-6">Minor Changes</a>
|
||||
- <a href="#bugfixes-30">Bugfixes</a>
|
||||
- <a href="#v1-3-0">v1\.3\.0</a>
|
||||
- <a href="#release-summary-34">Release Summary</a>
|
||||
- <a href="#minor-changes-7">Minor Changes</a>
|
||||
- <a href="#bugfixes-31">Bugfixes</a>
|
||||
- <a href="#new-modules-1">New Modules</a>
|
||||
- <a href="#v1-2-0">v1\.2\.0</a>
|
||||
- <a href="#release-summary-35">Release Summary</a>
|
||||
- <a href="#minor-changes-8">Minor Changes</a>
|
||||
- <a href="#security-fixes">Security Fixes</a>
|
||||
- <a href="#bugfixes-32">Bugfixes</a>
|
||||
- <a href="#v1-1-1">v1\.1\.1</a>
|
||||
- <a href="#release-summary-36">Release Summary</a>
|
||||
- <a href="#bugfixes-33">Bugfixes</a>
|
||||
- <a href="#v1-1-0">v1\.1\.0</a>
|
||||
- <a href="#release-summary-37">Release Summary</a>
|
||||
- <a href="#minor-changes-9">Minor Changes</a>
|
||||
- <a href="#bugfixes-34">Bugfixes</a>
|
||||
- <a href="#new-modules-2">New Modules</a>
|
||||
- <a href="#v1-0-0">v1\.0\.0</a>
|
||||
- <a href="#release-summary-38">Release Summary</a>
|
||||
- <a href="#minor-changes-10">Minor Changes</a>
|
||||
- <a href="#deprecated-features-2">Deprecated Features</a>
|
||||
- <a href="#removed-features-previously-deprecated">Removed Features \(previously deprecated\)</a>
|
||||
- <a href="#bugfixes-35">Bugfixes</a>
|
||||
- <a href="#new-modules-3">New Modules</a>
|
||||
|
||||
<a id="v1-9-26"></a>
|
||||
## v1\.9\.26
|
||||
|
||||
<a id="release-summary"></a>
|
||||
### Release Summary
|
||||
|
||||
Last release\.
|
||||
|
||||
<a id="major-changes"></a>
|
||||
### Major Changes
|
||||
|
||||
* The 1\.x\.y release train of community\.crypto is <strong>End of Life</strong>\. There will be no further community\.crypto 1\.x\.y releases\.
|
||||
Please upgrade to community\.crypto 2\.x\.y\.
|
||||
|
||||
Thanks to everyone who contributed to community\.crypto 1\.x\.y\!
|
||||
|
||||
<a id="v1-9-25"></a>
|
||||
## v1\.9\.25
|
||||
|
||||
<a id="release-summary-1"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes"></a>
|
||||
### Bugfixes
|
||||
|
||||
* crypto\.math module utils \- change return values for <code>quick\_is\_not\_prime\(\)</code> for special cases that do not appear when using the collection \([https\://github\.com/ansible\-collections/community\.crypto/pull/733](https\://github\.com/ansible\-collections/community\.crypto/pull/733)\)\.
|
||||
* ecs\_certificate \- fixed <code>csr</code> 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](https\://github\.com/ansible\-collections/community\.crypto/pull/740)\)\.
|
||||
|
||||
<a id="v1-9-24"></a>
|
||||
## v1\.9\.24
|
||||
|
||||
<a id="release-summary-2"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-1"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_dhparam \- was using an internal function instead of the public API to load DH param files when using the <code>cryptography</code> 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](https\://github\.com/ansible\-collections/community\.crypto/pull/698)\)\.
|
||||
* openssl\_privatekey\_info \- <code>check\_consistency\=true</code> no longer works for RSA keys with cryptography 42\.0\.0\+ \([https\://github\.com/ansible\-collections/community\.crypto/pull/701](https\://github\.com/ansible\-collections/community\.crypto/pull/701)\)\.
|
||||
* x509\_certificate \- when using the PyOpenSSL backend with <code>provider\=assertonly</code>\, better handle unexpected errors when validating private keys \([https\://github\.com/ansible\-collections/community\.crypto/pull/704](https\://github\.com/ansible\-collections/community\.crypto/pull/704)\)\.
|
||||
|
||||
<a id="v1-9-23"></a>
|
||||
## v1\.9\.23
|
||||
|
||||
<a id="release-summary-3"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-2"></a>
|
||||
### 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](https\://github\.com/ansible\-collections/community\.crypto/pull/666)\)\.
|
||||
|
||||
<a id="v1-9-22"></a>
|
||||
## v1\.9\.22
|
||||
|
||||
<a id="release-summary-4"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-3"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssh\_keypair \- always generate a new key pair if the private key does not exist\. Previously\, the module would fail when <code>regenerate\=fail</code> without an existing key\, contradicting the documentation \([https\://github\.com/ansible\-collections/community\.crypto/pull/598](https\://github\.com/ansible\-collections/community\.crypto/pull/598)\)\.
|
||||
|
||||
<a id="v1-9-21"></a>
|
||||
## v1\.9\.21
|
||||
|
||||
<a id="release-summary-5"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-4"></a>
|
||||
### Bugfixes
|
||||
|
||||
* action plugin helper \- fix handling of deprecations for ansible\-core 2\.14\.2 \([https\://github\.com/ansible\-collections/community\.crypto/pull/572](https\://github\.com/ansible\-collections/community\.crypto/pull/572)\)\.
|
||||
* openssl\_csr\, openssl\_csr\_pipe \- prevent invalid values for <code>crl\_distribution\_points</code> that do not have one of <code>full\_name</code>\, <code>relative\_name</code>\, and <code>crl\_issuer</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/560](https\://github\.com/ansible\-collections/community\.crypto/pull/560)\)\.
|
||||
|
||||
<a id="v1-9-20"></a>
|
||||
## v1\.9\.20
|
||||
|
||||
<a id="release-summary-6"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-5"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_publickey\_info \- do not crash with internal error when public key cannot be parsed \([https\://github\.com/ansible\-collections/community\.crypto/pull/551](https\://github\.com/ansible\-collections/community\.crypto/pull/551)\)\.
|
||||
|
||||
<a id="v1-9-19"></a>
|
||||
## v1\.9\.19
|
||||
|
||||
<a id="release-summary-7"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-6"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_privatekey\_pipe \- ensure compatibility with newer versions of ansible\-core \([https\://github\.com/ansible\-collections/community\.crypto/pull/515](https\://github\.com/ansible\-collections/community\.crypto/pull/515)\)\.
|
||||
|
||||
<a id="v1-9-18"></a>
|
||||
## v1\.9\.18
|
||||
|
||||
<a id="release-summary-8"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-7"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_pkcs12 \- when using the pyOpenSSL backend\, do not crash when trying to read non\-existing other certificates \([https\://github\.com/ansible\-collections/community\.crypto/issues/486](https\://github\.com/ansible\-collections/community\.crypto/issues/486)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/487](https\://github\.com/ansible\-collections/community\.crypto/pull/487)\)\.
|
||||
|
||||
<a id="v1-9-17"></a>
|
||||
## v1\.9\.17
|
||||
|
||||
<a id="release-summary-9"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-8"></a>
|
||||
### Bugfixes
|
||||
|
||||
* Include <code>Apache\-2\.0\.txt</code> file for <code>plugins/module\_utils/crypto/\_obj2txt\.py</code> and <code>plugins/module\_utils/crypto/\_objects\_data\.py</code>\.
|
||||
* openssl\_csr \- the module no longer crashes with \'permitted\_subtrees/excluded\_subtrees must be a non\-empty list or None\' if only one of <code>name\_constraints\_permitted</code> and <code>name\_constraints\_excluded</code> is provided \([https\://github\.com/ansible\-collections/community\.crypto/issues/481](https\://github\.com/ansible\-collections/community\.crypto/issues/481)\)\.
|
||||
* x509\_crl \- do not crash when signing CRL with Ed25519 or Ed448 keys \([https\://github\.com/ansible\-collections/community\.crypto/issues/473](https\://github\.com/ansible\-collections/community\.crypto/issues/473)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/474](https\://github\.com/ansible\-collections/community\.crypto/pull/474)\)\.
|
||||
|
||||
<a id="v1-9-16"></a>
|
||||
## v1\.9\.16
|
||||
|
||||
<a id="release-summary-10"></a>
|
||||
### Release Summary
|
||||
|
||||
Maintenance and bugfix release\.
|
||||
|
||||
<a id="bugfixes-9"></a>
|
||||
### Bugfixes
|
||||
|
||||
* Include <code>simplified\_bsd\.txt</code> license file for the ECS module utils\.
|
||||
* certificate\_complete\_chain \- do not stop execution if an unsupported signature algorithm is encountered\; warn instead \([https\://github\.com/ansible\-collections/community\.crypto/pull/457](https\://github\.com/ansible\-collections/community\.crypto/pull/457)\)\.
|
||||
|
||||
<a id="v1-9-15"></a>
|
||||
## v1\.9\.15
|
||||
|
||||
<a id="release-summary-11"></a>
|
||||
### Release Summary
|
||||
|
||||
Maintenance release\.
|
||||
|
||||
<a id="bugfixes-10"></a>
|
||||
### Bugfixes
|
||||
|
||||
* Include <code>PSF\-license\.txt</code> file for <code>plugins/module\_utils/\_version\.py</code>\.
|
||||
|
||||
<a id="v1-9-14"></a>
|
||||
## v1\.9\.14
|
||||
|
||||
<a id="release-summary-12"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-11"></a>
|
||||
### Bugfixes
|
||||
|
||||
* Make collection more robust when PyOpenSSL is used with an incompatible cryptography version \([https\://github\.com/ansible\-collections/community\.crypto/pull/446](https\://github\.com/ansible\-collections/community\.crypto/pull/446)\)\.
|
||||
* openssh\_\* modules \- fix exception handling to report traceback to users for enhanced traceability \([https\://github\.com/ansible\-collections/community\.crypto/pull/417](https\://github\.com/ansible\-collections/community\.crypto/pull/417)\)\.
|
||||
* x509\_crl \- fix crash when <code>issuer</code> for a revoked certificate is specified \([https\://github\.com/ansible\-collections/community\.crypto/pull/441](https\://github\.com/ansible\-collections/community\.crypto/pull/441)\)\.
|
||||
|
||||
<a id="v1-9-13"></a>
|
||||
## v1\.9\.13
|
||||
|
||||
<a id="release-summary-13"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-12"></a>
|
||||
### Bugfixes
|
||||
|
||||
* luks\_device \- fix parsing of <code>lsblk</code> output when device name ends with <code>crypt</code> \([https\://github\.com/ansible\-collections/community\.crypto/issues/409](https\://github\.com/ansible\-collections/community\.crypto/issues/409)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/410](https\://github\.com/ansible\-collections/community\.crypto/pull/410)\)\.
|
||||
|
||||
<a id="v1-9-12"></a>
|
||||
## v1\.9\.12
|
||||
|
||||
<a id="release-summary-14"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-13"></a>
|
||||
### Bugfixes
|
||||
|
||||
* certificate\_complete\_chain \- allow multiple potential intermediate certificates to have the same subject \([https\://github\.com/ansible\-collections/community\.crypto/issues/399](https\://github\.com/ansible\-collections/community\.crypto/issues/399)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/403](https\://github\.com/ansible\-collections/community\.crypto/pull/403)\)\.
|
||||
* x509\_certificate \- for the <code>ownca</code> provider\, check whether the CA private key actually belongs to the CA certificate\. This fix only covers the <code>cryptography</code> backend\, not the <code>pyopenssl</code> backend \([https\://github\.com/ansible\-collections/community\.crypto/pull/407](https\://github\.com/ansible\-collections/community\.crypto/pull/407)\)\.
|
||||
* x509\_certificate \- regenerate certificate when the CA\'s public key changes for <code>provider\=ownca</code>\. This fix only covers the <code>cryptography</code> backend\, not the <code>pyopenssl</code> backend \([https\://github\.com/ansible\-collections/community\.crypto/pull/407](https\://github\.com/ansible\-collections/community\.crypto/pull/407)\)\.
|
||||
* x509\_certificate \- regenerate certificate when the CA\'s subject changes for <code>provider\=ownca</code> \([https\://github\.com/ansible\-collections/community\.crypto/issues/400](https\://github\.com/ansible\-collections/community\.crypto/issues/400)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/402](https\://github\.com/ansible\-collections/community\.crypto/pull/402)\)\.
|
||||
* x509\_certificate \- regenerate certificate when the private key changes for <code>provider\=selfsigned</code>\. This fix only covers the <code>cryptography</code> backend\, not the <code>pyopenssl</code> backend \([https\://github\.com/ansible\-collections/community\.crypto/pull/407](https\://github\.com/ansible\-collections/community\.crypto/pull/407)\)\.
|
||||
|
||||
<a id="known-issues"></a>
|
||||
### Known Issues
|
||||
|
||||
* x509\_certificate \- when using the <code>ownca</code> provider with the <code>pyopenssl</code> backend\, changing the CA\'s public key does not cause regeneration of the certificate \([https\://github\.com/ansible\-collections/community\.crypto/pull/407](https\://github\.com/ansible\-collections/community\.crypto/pull/407)\)\.
|
||||
* x509\_certificate \- when using the <code>ownca</code> provider with the <code>pyopenssl</code> backend\, it is possible to specify a CA private key which is not related to the CA certificate \([https\://github\.com/ansible\-collections/community\.crypto/pull/407](https\://github\.com/ansible\-collections/community\.crypto/pull/407)\)\.
|
||||
* x509\_certificate \- when using the <code>selfsigned</code> provider with the <code>pyopenssl</code> backend\, changing the private key does not cause regeneration of the certificate \([https\://github\.com/ansible\-collections/community\.crypto/pull/407](https\://github\.com/ansible\-collections/community\.crypto/pull/407)\)\.
|
||||
|
||||
<a id="v1-9-11"></a>
|
||||
## v1\.9\.11
|
||||
|
||||
<a id="release-summary-15"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-14"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssh\_cert \- fixed false <code>changed</code> status for <code>host</code> certificates when using <code>full\_idempotence</code> \([https\://github\.com/ansible\-collections/community\.crypto/issues/395](https\://github\.com/ansible\-collections/community\.crypto/issues/395)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/396](https\://github\.com/ansible\-collections/community\.crypto/pull/396)\)\.
|
||||
|
||||
<a id="v1-9-10"></a>
|
||||
## v1\.9\.10
|
||||
|
||||
<a id="release-summary-16"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-15"></a>
|
||||
### Bugfixes
|
||||
|
||||
* luks\_devices \- set <code>LANG</code> and similar environment variables to avoid translated output\, which can break some of the module\'s functionality like key management \([https\://github\.com/ansible\-collections/community\.crypto/pull/388](https\://github\.com/ansible\-collections/community\.crypto/pull/388)\, [https\://github\.com/ansible\-collections/community\.crypto/issues/385](https\://github\.com/ansible\-collections/community\.crypto/issues/385)\)\.
|
||||
|
||||
<a id="v1-9-9"></a>
|
||||
## v1\.9\.9
|
||||
|
||||
<a id="bugfixes-16"></a>
|
||||
### Bugfixes
|
||||
|
||||
* Various modules and plugins \- use vendored version of <code>distutils\.version</code> instead of the deprecated Python standard library <code>distutils</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/353](https\://github\.com/ansible\-collections/community\.crypto/pull/353)\)\.
|
||||
* certificate\_complete\_chain \- do not append root twice if the chain already ends with a root certificate \([https\://github\.com/ansible\-collections/community\.crypto/pull/360](https\://github\.com/ansible\-collections/community\.crypto/pull/360)\)\.
|
||||
* certificate\_complete\_chain \- do not hang when infinite loop is found \([https\://github\.com/ansible\-collections/community\.crypto/issues/355](https\://github\.com/ansible\-collections/community\.crypto/issues/355)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/360](https\://github\.com/ansible\-collections/community\.crypto/pull/360)\)\.
|
||||
|
||||
<a id="v1-9-8"></a>
|
||||
## v1\.9\.8
|
||||
|
||||
<a id="release-summary-17"></a>
|
||||
### Release Summary
|
||||
|
||||
Documentation fix release\. No actual code changes\.
|
||||
|
||||
<a id="v1-9-7"></a>
|
||||
## v1\.9\.7
|
||||
|
||||
<a id="release-summary-18"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release with extra forward compatibility for newer versions of cryptography\.
|
||||
|
||||
<a id="minor-changes"></a>
|
||||
### Minor Changes
|
||||
|
||||
* acme\_\* modules \- fix usage of <code>fetch\_url</code> with changes in latest ansible\-core <code>devel</code> branch \([https\://github\.com/ansible\-collections/community\.crypto/pull/339](https\://github\.com/ansible\-collections/community\.crypto/pull/339)\)\.
|
||||
|
||||
<a id="bugfixes-17"></a>
|
||||
### Bugfixes
|
||||
|
||||
* acme\_certificate \- avoid passing multiple certificates to <code>cryptography</code>\'s X\.509 certificate loader when <code>fullchain\_dest</code> is used \([https\://github\.com/ansible\-collections/community\.crypto/pull/324](https\://github\.com/ansible\-collections/community\.crypto/pull/324)\)\.
|
||||
* get\_certificate\, openssl\_csr\_info\, x509\_certificate\_info \- add fallback code for extension parsing that works with cryptography 36\.0\.0 and newer\. This code re\-serializes de\-serialized extensions and thus can return slightly different values if the extension in the original CSR resp\. certificate was not canonicalized correctly\. This code is currently used as a fallback if the existing code stops working\, but we will switch it to be the main code in a future release \([https\://github\.com/ansible\-collections/community\.crypto/pull/331](https\://github\.com/ansible\-collections/community\.crypto/pull/331)\)\.
|
||||
* luks\_device \- now also runs a built\-in LUKS signature cleaner on <code>state\=absent</code> to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used \([https\://github\.com/ansible\-collections/community\.crypto/issues/326](https\://github\.com/ansible\-collections/community\.crypto/issues/326)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/327](https\://github\.com/ansible\-collections/community\.crypto/pull/327)\)\.
|
||||
* openssl\_pkcs12 \- use new PKCS\#12 deserialization infrastructure from cryptography 36\.0\.0 if available \([https\://github\.com/ansible\-collections/community\.crypto/pull/302](https\://github\.com/ansible\-collections/community\.crypto/pull/302)\)\.
|
||||
|
||||
<a id="v1-9-6"></a>
|
||||
## v1\.9\.6
|
||||
|
||||
<a id="release-summary-19"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-18"></a>
|
||||
### Bugfixes
|
||||
|
||||
* cryptography backend \- improve Unicode handling for Python 2 \([https\://github\.com/ansible\-collections/community\.crypto/pull/313](https\://github\.com/ansible\-collections/community\.crypto/pull/313)\)\.
|
||||
|
||||
<a id="v1-9-5"></a>
|
||||
## v1\.9\.5
|
||||
|
||||
<a id="release-summary-20"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release to fully support cryptography 35\.0\.0\.
|
||||
|
||||
<a id="bugfixes-19"></a>
|
||||
### Bugfixes
|
||||
|
||||
* get\_certificate \- fix compatibility with the cryptography 35\.0\.0 release \([https\://github\.com/ansible\-collections/community\.crypto/pull/294](https\://github\.com/ansible\-collections/community\.crypto/pull/294)\)\.
|
||||
* openssl\_csr\_info \- fix compatibility with the cryptography 35\.0\.0 release \([https\://github\.com/ansible\-collections/community\.crypto/pull/294](https\://github\.com/ansible\-collections/community\.crypto/pull/294)\)\.
|
||||
* openssl\_csr\_info \- fix compatibility with the cryptography 35\.0\.0 release in PyOpenSSL backend \([https\://github\.com/ansible\-collections/community\.crypto/pull/300](https\://github\.com/ansible\-collections/community\.crypto/pull/300)\)\.
|
||||
* openssl\_pkcs12 \- fix compatibility with the cryptography 35\.0\.0 release \([https\://github\.com/ansible\-collections/community\.crypto/pull/296](https\://github\.com/ansible\-collections/community\.crypto/pull/296)\)\.
|
||||
* x509\_certificate\_info \- fix compatibility with the cryptography 35\.0\.0 release \([https\://github\.com/ansible\-collections/community\.crypto/pull/294](https\://github\.com/ansible\-collections/community\.crypto/pull/294)\)\.
|
||||
* x509\_certificate\_info \- fix compatibility with the cryptography 35\.0\.0 release in PyOpenSSL backend \([https\://github\.com/ansible\-collections/community\.crypto/pull/300](https\://github\.com/ansible\-collections/community\.crypto/pull/300)\)\.
|
||||
|
||||
<a id="v1-9-4"></a>
|
||||
## v1\.9\.4
|
||||
|
||||
<a id="release-summary-21"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-20"></a>
|
||||
### Bugfixes
|
||||
|
||||
* acme\_\* modules \- fix commands composed for OpenSSL backend to retrieve information on CSRs and certificates from stdin to use <code>/dev/stdin</code> instead of <code>\-</code>\. This is needed for OpenSSL 1\.0\.1 and 1\.0\.2\, apparently \([https\://github\.com/ansible\-collections/community\.crypto/pull/279](https\://github\.com/ansible\-collections/community\.crypto/pull/279)\)\.
|
||||
* acme\_challenge\_cert\_helper \- only return exception when cryptography is not installed\, not when a too old version of it is installed\. This prevents Ansible\'s callback to crash \([https\://github\.com/ansible\-collections/community\.crypto/pull/281](https\://github\.com/ansible\-collections/community\.crypto/pull/281)\)\.
|
||||
|
||||
<a id="v1-9-3"></a>
|
||||
## v1\.9\.3
|
||||
|
||||
<a id="release-summary-22"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix release\.
|
||||
|
||||
<a id="bugfixes-21"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_csr and openssl\_csr\_pipe \- make sure that Unicode strings are used to compare strings with the cryptography backend\. This fixes idempotency problems with non\-ASCII letters on Python 2 \([https\://github\.com/ansible\-collections/community\.crypto/issues/270](https\://github\.com/ansible\-collections/community\.crypto/issues/270)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/271](https\://github\.com/ansible\-collections/community\.crypto/pull/271)\)\.
|
||||
|
||||
<a id="v1-9-2"></a>
|
||||
## v1\.9\.2
|
||||
|
||||
<a id="release-summary-23"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release to fix the changelog\. No other change compared to 1\.9\.0\.
|
||||
|
||||
<a id="v1-9-1"></a>
|
||||
## v1\.9\.1
|
||||
|
||||
<a id="release-summary-24"></a>
|
||||
### Release Summary
|
||||
|
||||
Accidental 1\.9\.1 release\. Identical to 1\.9\.0\.
|
||||
|
||||
<a id="v1-9-0"></a>
|
||||
## v1\.9\.0
|
||||
|
||||
<a id="release-summary-25"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular feature release\.
|
||||
|
||||
<a id="minor-changes-1"></a>
|
||||
### Minor Changes
|
||||
|
||||
* get\_certificate \- added <code>starttls</code> option to retrieve certificates from servers which require clients to request an encrypted connection \([https\://github\.com/ansible\-collections/community\.crypto/pull/264](https\://github\.com/ansible\-collections/community\.crypto/pull/264)\)\.
|
||||
* openssh\_keypair \- added <code>diff</code> support \([https\://github\.com/ansible\-collections/community\.crypto/pull/260](https\://github\.com/ansible\-collections/community\.crypto/pull/260)\)\.
|
||||
|
||||
<a id="bugfixes-22"></a>
|
||||
### Bugfixes
|
||||
|
||||
* keypair\_backend module utils \- simplify code to pass sanity tests \([https\://github\.com/ansible\-collections/community\.crypto/pull/263](https\://github\.com/ansible\-collections/community\.crypto/pull/263)\)\.
|
||||
* openssh\_keypair \- fixed <code>cryptography</code> backend to preserve original file permissions when regenerating a keypair requires existing files to be overwritten \([https\://github\.com/ansible\-collections/community\.crypto/pull/260](https\://github\.com/ansible\-collections/community\.crypto/pull/260)\)\.
|
||||
* openssh\_keypair \- fixed error handling to restore original keypair if regeneration fails \([https\://github\.com/ansible\-collections/community\.crypto/pull/260](https\://github\.com/ansible\-collections/community\.crypto/pull/260)\)\.
|
||||
* x509\_crl \- restore inherited function signature to pass sanity tests \([https\://github\.com/ansible\-collections/community\.crypto/pull/263](https\://github\.com/ansible\-collections/community\.crypto/pull/263)\)\.
|
||||
|
||||
<a id="v1-8-0"></a>
|
||||
## v1\.8\.0
|
||||
|
||||
<a id="release-summary-26"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix and feature release\.
|
||||
|
||||
<a id="minor-changes-2"></a>
|
||||
### Minor Changes
|
||||
|
||||
* Avoid internal ansible\-core module\_utils in favor of equivalent public API available since at least Ansible 2\.9 \([https\://github\.com/ansible\-collections/community\.crypto/pull/253](https\://github\.com/ansible\-collections/community\.crypto/pull/253)\)\.
|
||||
* openssh certificate module utils \- new module\_utils for parsing OpenSSH certificates \([https\://github\.com/ansible\-collections/community\.crypto/pull/246](https\://github\.com/ansible\-collections/community\.crypto/pull/246)\)\.
|
||||
* openssh\_cert \- added <code>regenerate</code> option to validate additional certificate parameters which trigger regeneration of an existing certificate \([https\://github\.com/ansible\-collections/community\.crypto/pull/256](https\://github\.com/ansible\-collections/community\.crypto/pull/256)\)\.
|
||||
* openssh\_cert \- adding <code>diff</code> support \([https\://github\.com/ansible\-collections/community\.crypto/pull/255](https\://github\.com/ansible\-collections/community\.crypto/pull/255)\)\.
|
||||
|
||||
<a id="bugfixes-23"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssh\_cert \- fixed certificate generation to restore original certificate if an error is encountered \([https\://github\.com/ansible\-collections/community\.crypto/pull/255](https\://github\.com/ansible\-collections/community\.crypto/pull/255)\)\.
|
||||
* openssh\_keypair \- fixed a bug that prevented custom file attributes being applied to public keys \([https\://github\.com/ansible\-collections/community\.crypto/pull/257](https\://github\.com/ansible\-collections/community\.crypto/pull/257)\)\.
|
||||
|
||||
<a id="v1-7-1"></a>
|
||||
## v1\.7\.1
|
||||
|
||||
<a id="release-summary-27"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-24"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_pkcs12 \- fix crash when loading passphrase\-protected PKCS\#12 files with <code>cryptography</code> backend \([https\://github\.com/ansible\-collections/community\.crypto/issues/247](https\://github\.com/ansible\-collections/community\.crypto/issues/247)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/248](https\://github\.com/ansible\-collections/community\.crypto/pull/248)\)\.
|
||||
|
||||
<a id="v1-7-0"></a>
|
||||
## v1\.7\.0
|
||||
|
||||
<a id="release-summary-28"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular feature and bugfix release\.
|
||||
|
||||
<a id="minor-changes-3"></a>
|
||||
### Minor Changes
|
||||
|
||||
* cryptography\_openssh module utils \- new module\_utils for managing asymmetric keypairs and OpenSSH formatted/encoded asymmetric keypairs \([https\://github\.com/ansible\-collections/community\.crypto/pull/213](https\://github\.com/ansible\-collections/community\.crypto/pull/213)\)\.
|
||||
* openssh\_keypair \- added <code>backend</code> parameter for selecting between the cryptography library or the OpenSSH binary for the execution of actions performed by <code>openssh\_keypair</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/236](https\://github\.com/ansible\-collections/community\.crypto/pull/236)\)\.
|
||||
* openssh\_keypair \- added <code>passphrase</code> parameter for encrypting/decrypting OpenSSH private keys \([https\://github\.com/ansible\-collections/community\.crypto/pull/225](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](https\://github\.com/ansible\-collections/community\.crypto/pull/150)\)\.
|
||||
* openssl\_csr\_info \- now returns <code>public\_key\_type</code> and <code>public\_key\_data</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/233](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](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](https\://github\.com/ansible\-collections/community\.crypto/pull/150)\)\.
|
||||
* openssl\_pkcs12 \- added option <code>select\_crypto\_backend</code> and a <code>cryptography</code> backend\. This requires cryptography 3\.0 or newer\, and does not support the <code>iter\_size</code> and <code>maciter\_size</code> options \([https\://github\.com/ansible\-collections/community\.crypto/pull/234](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](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](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](https\://github\.com/ansible\-collections/community\.crypto/pull/150)\)\.
|
||||
* x509\_certificate\_info \- now returns <code>public\_key\_type</code> and <code>public\_key\_data</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/233](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](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](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/issues/38)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/150](https\://github\.com/ansible\-collections/community\.crypto/pull/150)\)\.
|
||||
* x509\_crl\_info \- add <code>list\_revoked\_certificates</code> option to avoid enumerating all revoked certificates \([https\://github\.com/ansible\-collections/community\.crypto/pull/232](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](https\://github\.com/ansible\-collections/community\.crypto/pull/203)\)\.
|
||||
|
||||
<a id="bugfixes-25"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssh\_keypair \- fix <code>check\_mode</code> to populate return values for existing keypairs \([https\://github\.com/ansible\-collections/community\.crypto/issues/113](https\://github\.com/ansible\-collections/community\.crypto/issues/113)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/230](https\://github\.com/ansible\-collections/community\.crypto/pull/230)\)\.
|
||||
* various modules \- prevent crashes when modules try to set attributes on not yet existing files in check mode\. This will be fixed in ansible\-core 2\.12\, but it is not backported to every Ansible version we support \([https\://github\.com/ansible\-collections/community\.crypto/issue/242](https\://github\.com/ansible\-collections/community\.crypto/issue/242)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/243](https\://github\.com/ansible\-collections/community\.crypto/pull/243)\)\.
|
||||
* x509\_certificate \- fix crash when <code>assertonly</code> provider is used and some error conditions should be reported \([https\://github\.com/ansible\-collections/community\.crypto/issues/240](https\://github\.com/ansible\-collections/community\.crypto/issues/240)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/241](https\://github\.com/ansible\-collections/community\.crypto/pull/241)\)\.
|
||||
|
||||
<a id="new-modules"></a>
|
||||
### New Modules
|
||||
|
||||
* openssl\_publickey\_info \- Provide information for OpenSSL public keys
|
||||
|
||||
<a id="v1-6-2"></a>
|
||||
## v1\.6\.2
|
||||
|
||||
<a id="release-summary-29"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\. Fixes compatibility issue of ACME modules with step\-ca\.
|
||||
|
||||
<a id="bugfixes-26"></a>
|
||||
### Bugfixes
|
||||
|
||||
* acme\_\* modules \- avoid crashing for ACME servers where the <code>meta</code> directory key is not present \([https\://github\.com/ansible\-collections/community\.crypto/issues/220](https\://github\.com/ansible\-collections/community\.crypto/issues/220)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/221](https\://github\.com/ansible\-collections/community\.crypto/pull/221)\)\.
|
||||
|
||||
<a id="v1-6-1"></a>
|
||||
## v1\.6\.1
|
||||
|
||||
<a id="release-summary-30"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release\.
|
||||
|
||||
<a id="bugfixes-27"></a>
|
||||
### Bugfixes
|
||||
|
||||
* acme\_\* modules \- fix wrong usages of <code>ACMEProtocolException</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/216](https\://github\.com/ansible\-collections/community\.crypto/pull/216)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/217](https\://github\.com/ansible\-collections/community\.crypto/pull/217)\)\.
|
||||
|
||||
<a id="v1-6-0"></a>
|
||||
## v1\.6\.0
|
||||
|
||||
<a id="release-summary-31"></a>
|
||||
### Release Summary
|
||||
|
||||
Fixes compatibility issues with the latest ansible\-core 2\.11 beta\, and contains a lot of internal refactoring for the ACME modules and support for private key passphrases for them\.
|
||||
|
||||
<a id="minor-changes-4"></a>
|
||||
### Minor Changes
|
||||
|
||||
* acme module\_utils \- the <code>acme</code> module\_utils has been split up into several Python modules \([https\://github\.com/ansible\-collections/community\.crypto/pull/184](https\://github\.com/ansible\-collections/community\.crypto/pull/184)\)\.
|
||||
* acme\_\* modules \- codebase refactor which should not be visible to end\-users \([https\://github\.com/ansible\-collections/community\.crypto/pull/184](https\://github\.com/ansible\-collections/community\.crypto/pull/184)\)\.
|
||||
* acme\_\* modules \- support account key passphrases for <code>cryptography</code> backend \([https\://github\.com/ansible\-collections/community\.crypto/issues/197](https\://github\.com/ansible\-collections/community\.crypto/issues/197)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/207](https\://github\.com/ansible\-collections/community\.crypto/pull/207)\)\.
|
||||
* acme\_certificate\_revoke \- support revoking by private keys that are passphrase protected for <code>cryptography</code> backend \([https\://github\.com/ansible\-collections/community\.crypto/pull/207](https\://github\.com/ansible\-collections/community\.crypto/pull/207)\)\.
|
||||
* acme\_challenge\_cert\_helper \- add <code>private\_key\_passphrase</code> parameter \([https\://github\.com/ansible\-collections/community\.crypto/pull/207](https\://github\.com/ansible\-collections/community\.crypto/pull/207)\)\.
|
||||
|
||||
<a id="deprecated-features"></a>
|
||||
### Deprecated Features
|
||||
|
||||
* acme module\_utils \- the <code>acme</code> module\_utils \(<code>ansible\_collections\.community\.crypto\.plugins\.module\_utils\.acme</code>\) is deprecated and will be removed in community\.crypto 2\.0\.0\. Use the new Python modules in the <code>acme</code> package instead \(<code>ansible\_collections\.community\.crypto\.plugins\.module\_utils\.acme\.xxx</code>\) \([https\://github\.com/ansible\-collections/community\.crypto/pull/184](https\://github\.com/ansible\-collections/community\.crypto/pull/184)\)\.
|
||||
|
||||
<a id="bugfixes-28"></a>
|
||||
### Bugfixes
|
||||
|
||||
* action\_module plugin helper \- make compatible with latest changes in ansible\-core 2\.11\.0b3 \([https\://github\.com/ansible\-collections/community\.crypto/pull/202](https\://github\.com/ansible\-collections/community\.crypto/pull/202)\)\.
|
||||
* openssl\_privatekey\_pipe \- make compatible with latest changes in ansible\-core 2\.11\.0b3 \([https\://github\.com/ansible\-collections/community\.crypto/pull/202](https\://github\.com/ansible\-collections/community\.crypto/pull/202)\)\.
|
||||
|
||||
<a id="v1-5-0"></a>
|
||||
## v1\.5\.0
|
||||
|
||||
<a id="release-summary-32"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular feature and bugfix release\. Deprecates a return value\.
|
||||
|
||||
<a id="minor-changes-5"></a>
|
||||
### Minor Changes
|
||||
|
||||
* acme\_account\_info \- when <code>retrieve\_orders</code> is not <code>ignore</code> and the ACME server allows to query orders\, the new return value <code>order\_uris</code> is always populated with a list of URIs \([https\://github\.com/ansible\-collections/community\.crypto/pull/178](https\://github\.com/ansible\-collections/community\.crypto/pull/178)\)\.
|
||||
* luks\_device \- allow to specify sector size for LUKS2 containers with new <code>sector\_size</code> parameter \([https\://github\.com/ansible\-collections/community\.crypto/pull/193](https\://github\.com/ansible\-collections/community\.crypto/pull/193)\)\.
|
||||
|
||||
<a id="deprecated-features-1"></a>
|
||||
### Deprecated Features
|
||||
|
||||
* acme\_account\_info \- when <code>retrieve\_orders\=url\_list</code>\, <code>orders</code> will no longer be returned in community\.crypto 2\.0\.0\. Use <code>order\_uris</code> instead \([https\://github\.com/ansible\-collections/community\.crypto/pull/178](https\://github\.com/ansible\-collections/community\.crypto/pull/178)\)\.
|
||||
|
||||
<a id="bugfixes-29"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_csr \- no longer fails when comparing CSR without basic constraint when <code>basic\_constraints</code> is specified \([https\://github\.com/ansible\-collections/community\.crypto/issues/179](https\://github\.com/ansible\-collections/community\.crypto/issues/179)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/180](https\://github\.com/ansible\-collections/community\.crypto/pull/180)\)\.
|
||||
|
||||
<a id="v1-4-0"></a>
|
||||
## v1\.4\.0
|
||||
|
||||
<a id="release-summary-33"></a>
|
||||
### Release Summary
|
||||
|
||||
Release with several new features and bugfixes\.
|
||||
|
||||
<a id="minor-changes-6"></a>
|
||||
### Minor Changes
|
||||
|
||||
* The ACME module\_utils has been relicensed back from the Simplified BSD License \([https\://opensource\.org/licenses/BSD\-2\-Clause](https\://opensource\.org/licenses/BSD\-2\-Clause)\) to the GPLv3\+ \(same license used by most other code in this collection\)\. This undoes a licensing change when the original GPLv3\+ licensed code was moved to module\_utils in [https\://github\.com/ansible/ansible/pull/40697](https\://github\.com/ansible/ansible/pull/40697) \([https\://github\.com/ansible\-collections/community\.crypto/pull/165](https\://github\.com/ansible\-collections/community\.crypto/pull/165)\)\.
|
||||
* The <code>crypto/identify\.py</code> module\_utils has been renamed to <code>crypto/pem\.py</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/166](https\://github\.com/ansible\-collections/community\.crypto/pull/166)\)\.
|
||||
* luks\_device \- <code>new\_keyfile</code>\, <code>new\_passphrase</code>\, <code>remove\_keyfile</code> and <code>remove\_passphrase</code> are now idempotent \([https\://github\.com/ansible\-collections/community\.crypto/issues/19](https\://github\.com/ansible\-collections/community\.crypto/issues/19)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/168](https\://github\.com/ansible\-collections/community\.crypto/pull/168)\)\.
|
||||
* luks\_device \- allow to configure PBKDF \([https\://github\.com/ansible\-collections/community\.crypto/pull/163](https\://github\.com/ansible\-collections/community\.crypto/pull/163)\)\.
|
||||
* openssl\_csr\, openssl\_csr\_pipe \- allow to specify CRL distribution endpoints with <code>crl\_distribution\_points</code> \([https\://github\.com/ansible\-collections/community\.crypto/issues/147](https\://github\.com/ansible\-collections/community\.crypto/issues/147)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/167](https\://github\.com/ansible\-collections/community\.crypto/pull/167)\)\.
|
||||
* openssl\_pkcs12 \- allow to specify certificate bundles in <code>other\_certificates</code> by using new option <code>other\_certificates\_parse\_all</code> \([https\://github\.com/ansible\-collections/community\.crypto/issues/149](https\://github\.com/ansible\-collections/community\.crypto/issues/149)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/166](https\://github\.com/ansible\-collections/community\.crypto/pull/166)\)\.
|
||||
|
||||
<a id="bugfixes-30"></a>
|
||||
### Bugfixes
|
||||
|
||||
* acme\_certificate \- error when requested challenge type is not found for non\-valid challenges\, instead of hanging on step 2 \([https\://github\.com/ansible\-collections/community\.crypto/issues/171](https\://github\.com/ansible\-collections/community\.crypto/issues/171)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/173](https\://github\.com/ansible\-collections/community\.crypto/pull/173)\)\.
|
||||
|
||||
<a id="v1-3-0"></a>
|
||||
## v1\.3\.0
|
||||
|
||||
<a id="release-summary-34"></a>
|
||||
### Release Summary
|
||||
|
||||
Contains new modules <code>openssl\_privatekey\_pipe</code>\, <code>openssl\_csr\_pipe</code> and <code>x509\_certificate\_pipe</code> which allow to create or update private keys\, CSRs and X\.509 certificates without having to write them to disk\.
|
||||
|
||||
<a id="minor-changes-7"></a>
|
||||
### Minor Changes
|
||||
|
||||
* openssh\_cert \- add module parameter <code>use\_agent</code> to enable using signing keys stored in ssh\-agent \([https\://github\.com/ansible\-collections/community\.crypto/issues/116](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](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](https\://github\.com/ansible\-collections/community\.crypto/pull/119)\)\.
|
||||
* openssl\_privatekey \- the elliptic curve <code>secp192r1</code> 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](https\://github\.com/ansible\-collections/community\.crypto/pull/132)\)\.
|
||||
* x509\_certificate \- for the <code>selfsigned</code> 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/issues/32)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/129](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](https\://github\.com/ansible\-collections/community\.crypto/pull/135)\)\.
|
||||
|
||||
<a id="bugfixes-31"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_pkcs12 \- report the correct state when <code>action</code> is <code>parse</code> \([https\://github\.com/ansible\-collections/community\.crypto/issues/143](https\://github\.com/ansible\-collections/community\.crypto/issues/143)\)\.
|
||||
* support code \- improve handling of certificate and certificate signing request \(CSR\) loading with the <code>cryptography</code> backend when errors occur \([https\://github\.com/ansible\-collections/community\.crypto/issues/138](https\://github\.com/ansible\-collections/community\.crypto/issues/138)\, [https\://github\.com/ansible\-collections/community\.crypto/pull/139](https\://github\.com/ansible\-collections/community\.crypto/pull/139)\)\.
|
||||
* x509\_certificate \- fix <code>entrust</code> provider\, which was broken since community\.crypto 0\.1\.0 due to a feature added before the collection move \([https\://github\.com/ansible\-collections/community\.crypto/pull/135](https\://github\.com/ansible\-collections/community\.crypto/pull/135)\)\.
|
||||
|
||||
<a id="new-modules-1"></a>
|
||||
### 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
|
||||
|
||||
<a id="v1-2-0"></a>
|
||||
## v1\.2\.0
|
||||
|
||||
<a id="release-summary-35"></a>
|
||||
### Release Summary
|
||||
|
||||
Please note that this release fixes a security issue \(CVE\-2020\-25646\)\.
|
||||
|
||||
<a id="minor-changes-8"></a>
|
||||
### Minor Changes
|
||||
|
||||
* acme\_certificate \- allow to pass CSR file as content with new option <code>csr\_content</code> \([https\://github\.com/ansible\-collections/community\.crypto/pull/115](https\://github\.com/ansible\-collections/community\.crypto/pull/115)\)\.
|
||||
* x509\_certificate\_info \- add <code>fingerprints</code> return value which returns certificate fingerprints \([https\://github\.com/ansible\-collections/community\.crypto/pull/121](https\://github\.com/ansible\-collections/community\.crypto/pull/121)\)\.
|
||||
|
||||
<a id="security-fixes"></a>
|
||||
### Security Fixes
|
||||
|
||||
* openssl\_csr \- the option <code>privatekey\_content</code> was not marked as <code>no\_log</code>\, resulting in it being dumped into the system log by default\, and returned in the registered results in the <code>invocation</code> field \(CVE\-2020\-25646\, [https\://github\.com/ansible\-collections/community\.crypto/pull/125](https\://github\.com/ansible\-collections/community\.crypto/pull/125)\)\.
|
||||
* openssl\_privatekey\_info \- the option <code>content</code> was not marked as <code>no\_log</code>\, resulting in it being dumped into the system log by default\, and returned in the registered results in the <code>invocation</code> field \(CVE\-2020\-25646\, [https\://github\.com/ansible\-collections/community\.crypto/pull/125](https\://github\.com/ansible\-collections/community\.crypto/pull/125)\)\.
|
||||
* openssl\_publickey \- the option <code>privatekey\_content</code> was not marked as <code>no\_log</code>\, resulting in it being dumped into the system log by default\, and returned in the registered results in the <code>invocation</code> field \(CVE\-2020\-25646\, [https\://github\.com/ansible\-collections/community\.crypto/pull/125](https\://github\.com/ansible\-collections/community\.crypto/pull/125)\)\.
|
||||
* openssl\_signature \- the option <code>privatekey\_content</code> was not marked as <code>no\_log</code>\, resulting in it being dumped into the system log by default\, and returned in the registered results in the <code>invocation</code> field \(CVE\-2020\-25646\, [https\://github\.com/ansible\-collections/community\.crypto/pull/125](https\://github\.com/ansible\-collections/community\.crypto/pull/125)\)\.
|
||||
* x509\_certificate \- the options <code>privatekey\_content</code> and <code>ownca\_privatekey\_content</code> were not marked as <code>no\_log</code>\, resulting in it being dumped into the system log by default\, and returned in the registered results in the <code>invocation</code> field \(CVE\-2020\-25646\, [https\://github\.com/ansible\-collections/community\.crypto/pull/125](https\://github\.com/ansible\-collections/community\.crypto/pull/125)\)\.
|
||||
* x509\_crl \- the option <code>privatekey\_content</code> was not marked as <code>no\_log</code>\, resulting in it being dumped into the system log by default\, and returned in the registered results in the <code>invocation</code> field \(CVE\-2020\-25646\, [https\://github\.com/ansible\-collections/community\.crypto/pull/125](https\://github\.com/ansible\-collections/community\.crypto/pull/125)\)\.
|
||||
|
||||
<a id="bugfixes-32"></a>
|
||||
### Bugfixes
|
||||
|
||||
* openssl\_pkcs12 \- do not crash when reading PKCS\#12 file which has no private key and/or no main certificate \([https\://github\.com/ansible\-collections/community\.crypto/issues/103](https\://github\.com/ansible\-collections/community\.crypto/issues/103)\)\.
|
||||
|
||||
<a id="v1-1-1"></a>
|
||||
## v1\.1\.1
|
||||
|
||||
<a id="release-summary-36"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfixes for Ansible 2\.10\.0\.
|
||||
|
||||
<a id="bugfixes-33"></a>
|
||||
### Bugfixes
|
||||
|
||||
* meta/runtime\.yml \- convert Ansible version numbers for old names of modules to collection version numbers \([https\://github\.com/ansible\-collections/community\.crypto/pull/108](https\://github\.com/ansible\-collections/community\.crypto/pull/108)\)\.
|
||||
* openssl\_csr \- improve handling of IDNA errors \([https\://github\.com/ansible\-collections/community\.crypto/issues/105](https\://github\.com/ansible\-collections/community\.crypto/issues/105)\)\.
|
||||
|
||||
<a id="v1-1-0"></a>
|
||||
## v1\.1\.0
|
||||
|
||||
<a id="release-summary-37"></a>
|
||||
### Release Summary
|
||||
|
||||
Release for Ansible 2\.10\.0\.
|
||||
|
||||
<a id="minor-changes-9"></a>
|
||||
### Minor Changes
|
||||
|
||||
* acme\_account \- add <code>external\_account\_binding</code> option to allow creation of ACME accounts with External Account Binding \([https\://github\.com/ansible\-collections/community\.crypto/issues/89](https\://github\.com/ansible\-collections/community\.crypto/issues/89)\)\.
|
||||
* acme\_certificate \- allow new selector <code>test\_certificates\: first</code> for <code>select\_chain</code> parameter \([https\://github\.com/ansible\-collections/community\.crypto/pull/102](https\://github\.com/ansible\-collections/community\.crypto/pull/102)\)\.
|
||||
* cryptography backends \- support arbitrary dotted OIDs \([https\://github\.com/ansible\-collections/community\.crypto/issues/39](https\://github\.com/ansible\-collections/community\.crypto/issues/39)\)\.
|
||||
* get\_certificate \- add support for SNI \([https\://github\.com/ansible\-collections/community\.crypto/issues/69](https\://github\.com/ansible\-collections/community\.crypto/issues/69)\)\.
|
||||
* luks\_device \- add support for encryption options on container creation \([https\://github\.com/ansible\-collections/community\.crypto/pull/97](https\://github\.com/ansible\-collections/community\.crypto/pull/97)\)\.
|
||||
* openssh\_cert \- add support for PKCS\#11 tokens \([https\://github\.com/ansible\-collections/community\.crypto/pull/95](https\://github\.com/ansible\-collections/community\.crypto/pull/95)\)\.
|
||||
* openssl\_certificate \- the PyOpenSSL backend now uses 160 bits of randomness for serial numbers\, instead of a random number between 1000 and 99999\. Please note that this is not a high quality random number \([https\://github\.com/ansible\-collections/community\.crypto/issues/76](https\://github\.com/ansible\-collections/community\.crypto/issues/76)\)\.
|
||||
* openssl\_csr \- add support for name constraints extension \([https\://github\.com/ansible\-collections/community\.crypto/issues/46](https\://github\.com/ansible\-collections/community\.crypto/issues/46)\)\.
|
||||
* openssl\_csr\_info \- add support for name constraints extension \([https\://github\.com/ansible\-collections/community\.crypto/issues/46](https\://github\.com/ansible\-collections/community\.crypto/issues/46)\)\.
|
||||
|
||||
<a id="bugfixes-34"></a>
|
||||
### Bugfixes
|
||||
|
||||
* acme\_inspect \- fix problem with Python 3\.5 that JSON was not decoded \([https\://github\.com/ansible\-collections/community\.crypto/issues/86](https\://github\.com/ansible\-collections/community\.crypto/issues/86)\)\.
|
||||
* get\_certificate \- fix <code>ca\_cert</code> option handling when <code>proxy\_host</code> is used \([https\://github\.com/ansible\-collections/community\.crypto/pull/84](https\://github\.com/ansible\-collections/community\.crypto/pull/84)\)\.
|
||||
* openssl\_\*\, x509\_\* modules \- fix handling of general names which refer to IP networks and not IP addresses \([https\://github\.com/ansible\-collections/community\.crypto/pull/92](https\://github\.com/ansible\-collections/community\.crypto/pull/92)\)\.
|
||||
|
||||
<a id="new-modules-2"></a>
|
||||
### New Modules
|
||||
|
||||
* openssl\_signature \- Sign data with openssl
|
||||
* openssl\_signature\_info \- Verify signatures with openssl
|
||||
|
||||
<a id="v1-0-0"></a>
|
||||
## v1\.0\.0
|
||||
|
||||
<a id="release-summary-38"></a>
|
||||
### Release Summary
|
||||
|
||||
This is the first proper release of the <code>community\.crypto</code> collection\. This changelog contains all changes to the modules in this collection that were added after the release of Ansible 2\.9\.0\.
|
||||
|
||||
<a id="minor-changes-10"></a>
|
||||
### Minor Changes
|
||||
|
||||
* luks\_device \- accept <code>passphrase</code>\, <code>new\_passphrase</code> and <code>remove\_passphrase</code>\.
|
||||
* luks\_device \- add <code>keysize</code> parameter to set key size at LUKS container creation
|
||||
* luks\_device \- added support to use UUIDs\, and labels with LUKS2 containers
|
||||
* luks\_device \- added the <code>type</code> option that allows user explicit define the LUKS container format version
|
||||
* openssh\_keypair \- instead of regenerating some broken or password protected keys\, fail the module\. Keys can still be regenerated by calling the module with <code>force\=yes</code>\.
|
||||
* openssh\_keypair \- the <code>regenerate</code> 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 <code>dirName</code>\, <code>otherName</code> and <code>RID</code> \(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 \- Change the required version of acme\-tiny to \>\= 4\.0\.0
|
||||
* openssl\_certificate \- allow to provide content of some input files via the <code>csr\_content</code>\, <code>privatekey\_content</code>\, <code>ownca\_privatekey\_content</code> and <code>ownca\_content</code> options\.
|
||||
* openssl\_certificate \- allow to return the existing/generated certificate directly as <code>certificate</code> by setting <code>return\_content</code> to <code>yes</code>\.
|
||||
* openssl\_certificate\_info \- allow to provide certificate content via <code>content</code> option \([https\://github\.com/ansible/ansible/issues/64776](https\://github\.com/ansible/ansible/issues/64776)\)\.
|
||||
* openssl\_csr \- Add support for specifying the SAN <code>otherName</code> value in the OpenSSL ASN\.1 UTF8 string format\, <code>otherName\:\<OID\>\;UTF8\:string value</code>\.
|
||||
* openssl\_csr \- allow to provide private key content via <code>private\_key\_content</code> option\.
|
||||
* openssl\_csr \- allow to return the existing/generated CSR directly as <code>csr</code> by setting <code>return\_content</code> to <code>yes</code>\.
|
||||
* openssl\_csr\_info \- allow to provide CSR content via <code>content</code> option\.
|
||||
* openssl\_dhparam \- allow to return the existing/generated DH params directly as <code>dhparams</code> by setting <code>return\_content</code> to <code>yes</code>\.
|
||||
* openssl\_dhparam \- now supports a <code>cryptography</code>\-based backend\. Auto\-detection can be overwritten with the <code>select\_crypto\_backend</code> option\.
|
||||
* openssl\_pkcs12 \- allow to return the existing/generated PKCS\#12 directly as <code>pkcs12</code> by setting <code>return\_content</code> to <code>yes</code>\.
|
||||
* openssl\_privatekey \- add <code>format</code> and <code>format\_mismatch</code> options\.
|
||||
* openssl\_privatekey \- allow to return the existing/generated private key directly as <code>privatekey</code> by setting <code>return\_content</code> to <code>yes</code>\.
|
||||
* openssl\_privatekey \- the <code>regenerate</code> option allows to configure the module\'s behavior when it should or needs to regenerate private keys\.
|
||||
* openssl\_privatekey\_info \- allow to provide private key content via <code>content</code> option\.
|
||||
* openssl\_publickey \- allow to provide private key content via <code>private\_key\_content</code> option\.
|
||||
* openssl\_publickey \- allow to return the existing/generated public key directly as <code>publickey</code> by setting <code>return\_content</code> to <code>yes</code>\.
|
||||
|
||||
<a id="deprecated-features-2"></a>
|
||||
### Deprecated Features
|
||||
|
||||
* openssl\_csr \- all values for the <code>version</code> option except <code>1</code> are deprecated\. The value 1 denotes the current only standardized CSR version\.
|
||||
|
||||
<a id="removed-features-previously-deprecated"></a>
|
||||
### Removed Features \(previously deprecated\)
|
||||
|
||||
* The <code>letsencrypt</code> module has been removed\. Use <code>acme\_certificate</code> instead\.
|
||||
|
||||
<a id="bugfixes-35"></a>
|
||||
### Bugfixes
|
||||
|
||||
* ACME modules\: fix bug in ACME v1 account update code
|
||||
* ACME modules\: make sure some connection errors are handled properly
|
||||
* ACME modules\: support Buypass\' ACME v1 endpoint
|
||||
* acme\_certificate \- fix crash when module is used with Python 2\.x\.
|
||||
* acme\_certificate \- fix misbehavior when ACME v1 is used with <code>modify\_account</code> set to <code>false</code>\.
|
||||
* ecs\_certificate \- Always specify header <code>connection\: keep\-alive</code> for ECS API connections\.
|
||||
* ecs\_certificate \- Fix formatting of contents of <code>full\_chain\_path</code>\.
|
||||
* get\_certificate \- Fix cryptography backend when pyopenssl is unavailable \([https\://github\.com/ansible/ansible/issues/67900](https\://github\.com/ansible/ansible/issues/67900)\)
|
||||
* openssh\_keypair \- add logic to avoid breaking password protected keys\.
|
||||
* openssh\_keypair \- fixes idempotence issue with public key \([https\://github\.com/ansible/ansible/issues/64969](https\://github\.com/ansible/ansible/issues/64969)\)\.
|
||||
* openssh\_keypair \- public key\'s file attributes \(permissions\, owner\, group\, etc\.\) are now set to the same values as the private key\.
|
||||
* openssl\_\* modules \- prevent crash on fingerprint determination in FIPS mode \([https\://github\.com/ansible/ansible/issues/67213](https\://github\.com/ansible/ansible/issues/67213)\)\.
|
||||
* openssl\_certificate \- When provider is <code>entrust</code>\, use a <code>connection\: keep\-alive</code> header for ECS API connections\.
|
||||
* openssl\_certificate \- <code>provider</code> option was documented as required\, but it was not checked whether it was provided\. It is now only required when <code>state</code> is <code>present</code>\.
|
||||
* openssl\_certificate \- fix <code>assertonly</code> provider certificate verification\, causing \'private key mismatch\' and \'subject mismatch\' errors\.
|
||||
* openssl\_certificate and openssl\_csr \- fix Ed25519 and Ed448 private key support for <code>cryptography</code> backend\. This probably needs at least cryptography 2\.8\, since older versions have problems with signing certificates or CSRs with such keys\. \([https\://github\.com/ansible/ansible/issues/59039](https\://github\.com/ansible/ansible/issues/59039)\, PR [https\://github\.com/ansible/ansible/pull/63984](https\://github\.com/ansible/ansible/pull/63984)\)
|
||||
* openssl\_csr \- a warning is issued if an unsupported value for <code>version</code> is used for the <code>cryptography</code> backend\.
|
||||
* openssl\_csr \- the module will now enforce that <code>privatekey\_path</code> is specified when <code>state\=present</code>\.
|
||||
* openssl\_publickey \- fix a module crash caused when pyOpenSSL is not installed \([https\://github\.com/ansible/ansible/issues/67035](https\://github\.com/ansible/ansible/issues/67035)\)\.
|
||||
|
||||
<a id="new-modules-3"></a>
|
||||
### 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\)
|
||||
3
CHANGELOG.md.license
Normal file
3
CHANGELOG.md.license
Normal 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
|
||||
667
CHANGELOG.rst
667
CHANGELOG.rst
@@ -4,6 +4,672 @@ Community Crypto Release Notes
|
||||
|
||||
.. contents:: Topics
|
||||
|
||||
v1.9.26
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Last release.
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- The 1.x.y release train of community.crypto is **End of Life**. There will be no further community.crypto 1.x.y releases.
|
||||
Please upgrade to community.crypto 2.x.y.
|
||||
|
||||
Thanks to everyone who contributed to community.crypto 1.x.y!
|
||||
|
||||
v1.9.25
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- crypto.math module utils - change return values for ``quick_is_not_prime()`` 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).
|
||||
|
||||
v1.9.24
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
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).
|
||||
- x509_certificate - when using the PyOpenSSL backend with ``provider=assertonly``, better handle unexpected errors when validating private keys (https://github.com/ansible-collections/community.crypto/pull/704).
|
||||
|
||||
v1.9.23
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
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).
|
||||
|
||||
v1.9.22
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_keypair - always generate a new key pair if the private key does not exist. Previously, the module would fail when ``regenerate=fail`` without an existing key, contradicting the documentation (https://github.com/ansible-collections/community.crypto/pull/598).
|
||||
|
||||
v1.9.21
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- action plugin helper - fix handling of deprecations for ansible-core 2.14.2 (https://github.com/ansible-collections/community.crypto/pull/572).
|
||||
- openssl_csr, openssl_csr_pipe - prevent invalid values for ``crl_distribution_points`` that do not have one of ``full_name``, ``relative_name``, and ``crl_issuer`` (https://github.com/ansible-collections/community.crypto/pull/560).
|
||||
|
||||
v1.9.20
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_publickey_info - do not crash with internal error when public key cannot be parsed (https://github.com/ansible-collections/community.crypto/pull/551).
|
||||
|
||||
v1.9.19
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_privatekey_pipe - ensure compatibility with newer versions of ansible-core (https://github.com/ansible-collections/community.crypto/pull/515).
|
||||
|
||||
v1.9.18
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_pkcs12 - when using the pyOpenSSL backend, do not crash when trying to read non-existing other certificates (https://github.com/ansible-collections/community.crypto/issues/486, https://github.com/ansible-collections/community.crypto/pull/487).
|
||||
|
||||
v1.9.17
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Include ``Apache-2.0.txt`` file for ``plugins/module_utils/crypto/_obj2txt.py`` and ``plugins/module_utils/crypto/_objects_data.py``.
|
||||
- openssl_csr - the module no longer crashes with 'permitted_subtrees/excluded_subtrees must be a non-empty list or None' if only one of ``name_constraints_permitted`` and ``name_constraints_excluded`` is provided (https://github.com/ansible-collections/community.crypto/issues/481).
|
||||
- x509_crl - do not crash when signing CRL with Ed25519 or Ed448 keys (https://github.com/ansible-collections/community.crypto/issues/473, https://github.com/ansible-collections/community.crypto/pull/474).
|
||||
|
||||
v1.9.16
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Maintenance and bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Include ``simplified_bsd.txt`` license file for the ECS module utils.
|
||||
- certificate_complete_chain - do not stop execution if an unsupported signature algorithm is encountered; warn instead (https://github.com/ansible-collections/community.crypto/pull/457).
|
||||
|
||||
v1.9.15
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Maintenance release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``.
|
||||
|
||||
v1.9.14
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Make collection more robust when PyOpenSSL is used with an incompatible cryptography version (https://github.com/ansible-collections/community.crypto/pull/446).
|
||||
- openssh_* modules - fix exception handling to report traceback to users for enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417).
|
||||
- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified (https://github.com/ansible-collections/community.crypto/pull/441).
|
||||
|
||||
v1.9.13
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt`` (https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410).
|
||||
|
||||
v1.9.12
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- certificate_complete_chain - allow multiple potential intermediate certificates to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399, https://github.com/ansible-collections/community.crypto/pull/403).
|
||||
- x509_certificate - for the ``ownca`` provider, check whether the CA private key actually belongs to the CA certificate. This fix only covers the ``cryptography`` backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's public key changes for ``provider=ownca``. This fix only covers the ``cryptography`` backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's subject changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400, https://github.com/ansible-collections/community.crypto/pull/402).
|
||||
- x509_certificate - regenerate certificate when the private key changes for ``provider=selfsigned``. This fix only covers the ``cryptography`` backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl`` backend, changing the CA's public key does not cause regeneration of the certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl`` backend, it is possible to specify a CA private key which is not related to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``selfsigned`` provider with the ``pyopenssl`` backend, changing the private key does not cause regeneration of the certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
|
||||
v1.9.11
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_cert - fixed false ``changed`` status for ``host`` certificates when using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395, https://github.com/ansible-collections/community.crypto/pull/396).
|
||||
|
||||
v1.9.10
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- luks_devices - set ``LANG`` and similar environment variables to avoid translated output, which can break some of the module's functionality like key management (https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385).
|
||||
|
||||
v1.9.9
|
||||
======
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353).
|
||||
- certificate_complete_chain - do not append root twice if the chain already ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355, https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
|
||||
v1.9.8
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Documentation fix release. No actual code changes.
|
||||
|
||||
v1.9.7
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release with extra forward compatibility for newer versions of cryptography.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code for extension parsing that works with cryptography 36.0.0 and newer. This code re-serializes de-serialized extensions and thus can return slightly different values if the extension in the original CSR resp. certificate was not canonicalized correctly. This code is currently used as a fallback if the existing code stops working, but we will switch it to be the main code in a future release (https://github.com/ansible-collections/community.crypto/pull/331).
|
||||
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, https://github.com/ansible-collections/community.crypto/pull/327).
|
||||
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||
|
||||
v1.9.6
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||
|
||||
v1.9.5
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release to fully support cryptography 35.0.0.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
|
||||
v1.9.4
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``. This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||
- acme_challenge_cert_helper - only return exception when cryptography is not installed, not when a too old version of it is installed. This prevents Ansible's callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||
|
||||
v1.9.3
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used to compare strings with the cryptography backend. This fixes idempotency problems with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270, https://github.com/ansible-collections/community.crypto/pull/271).
|
||||
|
||||
v1.9.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release to fix the changelog. No other change compared to 1.9.0.
|
||||
|
||||
v1.9.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
|
||||
v1.9.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file permissions when regenerating a keypair requires existing files to be overwritten (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
|
||||
v1.8.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates (https://github.com/ansible-collections/community.crypto/pull/246).
|
||||
- openssh_cert - added ``regenerate`` option to validate additional certificate parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_cert - fixed certificate generation to restore original certificate if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
- openssh_keypair - fixed a bug that prevented custom file attributes being applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||
|
||||
v1.7.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247, https://github.com/ansible-collections/community.crypto/pull/248).
|
||||
|
||||
v1.7.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature and bugfix release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- cryptography_openssh module utils - new module_utils for managing asymmetric keypairs and OpenSSH formatted/encoded asymmetric keypairs (https://github.com/ansible-collections/community.crypto/pull/213).
|
||||
- openssh_keypair - added ``backend`` parameter for selecting between the cryptography library or the OpenSSH binary for the execution of actions performed by ``openssh_keypair`` (https://github.com/ansible-collections/community.crypto/pull/236).
|
||||
- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).
|
||||
- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
|
||||
- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
|
||||
- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234).
|
||||
- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_privatekey_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/205).
|
||||
- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
|
||||
- x509_certificate_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/206).
|
||||
- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232).
|
||||
- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_keypair - fix ``check_mode`` to populate return values for existing keypairs (https://github.com/ansible-collections/community.crypto/issues/113, https://github.com/ansible-collections/community.crypto/pull/230).
|
||||
- various modules - prevent crashes when modules try to set attributes on not yet existing files in check mode. This will be fixed in ansible-core 2.12, but it is not backported to every Ansible version we support (https://github.com/ansible-collections/community.crypto/issue/242, https://github.com/ansible-collections/community.crypto/pull/243).
|
||||
- x509_certificate - fix crash when ``assertonly`` provider is used and some error conditions should be reported (https://github.com/ansible-collections/community.crypto/issues/240, https://github.com/ansible-collections/community.crypto/pull/241).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- openssl_publickey_info - Provide information for OpenSSL public keys
|
||||
|
||||
v1.6.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release. Fixes compatibility issue of ACME modules with step-ca.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_* modules - avoid crashing for ACME servers where the ``meta`` directory key is not present (https://github.com/ansible-collections/community.crypto/issues/220, https://github.com/ansible-collections/community.crypto/pull/221).
|
||||
|
||||
v1.6.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_* modules - fix wrong usages of ``ACMEProtocolException`` (https://github.com/ansible-collections/community.crypto/pull/216, https://github.com/ansible-collections/community.crypto/pull/217).
|
||||
|
||||
v1.6.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Fixes compatibility issues with the latest ansible-core 2.11 beta, and contains a lot of internal refactoring for the ACME modules and support for private key passphrases for them.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme module_utils - the ``acme`` module_utils has been split up into several Python modules (https://github.com/ansible-collections/community.crypto/pull/184).
|
||||
- acme_* modules - codebase refactor which should not be visible to end-users (https://github.com/ansible-collections/community.crypto/pull/184).
|
||||
- acme_* modules - support account key passphrases for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/197, https://github.com/ansible-collections/community.crypto/pull/207).
|
||||
- acme_certificate_revoke - support revoking by private keys that are passphrase protected for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/pull/207).
|
||||
- acme_challenge_cert_helper - add ``private_key_passphrase`` parameter (https://github.com/ansible-collections/community.crypto/pull/207).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- acme module_utils - the ``acme`` module_utils (``ansible_collections.community.crypto.plugins.module_utils.acme``) is deprecated and will be removed in community.crypto 2.0.0. Use the new Python modules in the ``acme`` package instead (``ansible_collections.community.crypto.plugins.module_utils.acme.xxx``) (https://github.com/ansible-collections/community.crypto/pull/184).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- action_module plugin helper - make compatible with latest changes in ansible-core 2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202).
|
||||
- openssl_privatekey_pipe - make compatible with latest changes in ansible-core 2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202).
|
||||
|
||||
v1.5.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature and bugfix release. Deprecates a return value.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_account_info - when ``retrieve_orders`` is not ``ignore`` and the ACME server allows to query orders, the new return value ``order_uris`` is always populated with a list of URIs (https://github.com/ansible-collections/community.crypto/pull/178).
|
||||
- luks_device - allow to specify sector size for LUKS2 containers with new ``sector_size`` parameter (https://github.com/ansible-collections/community.crypto/pull/193).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- acme_account_info - when ``retrieve_orders=url_list``, ``orders`` will no longer be returned in community.crypto 2.0.0. Use ``order_uris`` instead (https://github.com/ansible-collections/community.crypto/pull/178).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_csr - no longer fails when comparing CSR without basic constraint when ``basic_constraints`` is specified (https://github.com/ansible-collections/community.crypto/issues/179, https://github.com/ansible-collections/community.crypto/pull/180).
|
||||
|
||||
v1.4.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Release with several new features and bugfixes.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- The ACME module_utils has been relicensed back from the Simplified BSD License (https://opensource.org/licenses/BSD-2-Clause) to the GPLv3+ (same license used by most other code in this collection). This undoes a licensing change when the original GPLv3+ licensed code was moved to module_utils in https://github.com/ansible/ansible/pull/40697 (https://github.com/ansible-collections/community.crypto/pull/165).
|
||||
- The ``crypto/identify.py`` module_utils has been renamed to ``crypto/pem.py`` (https://github.com/ansible-collections/community.crypto/pull/166).
|
||||
- luks_device - ``new_keyfile``, ``new_passphrase``, ``remove_keyfile`` and ``remove_passphrase`` are now idempotent (https://github.com/ansible-collections/community.crypto/issues/19, https://github.com/ansible-collections/community.crypto/pull/168).
|
||||
- luks_device - allow to configure PBKDF (https://github.com/ansible-collections/community.crypto/pull/163).
|
||||
- openssl_csr, openssl_csr_pipe - allow to specify CRL distribution endpoints with ``crl_distribution_points`` (https://github.com/ansible-collections/community.crypto/issues/147, https://github.com/ansible-collections/community.crypto/pull/167).
|
||||
- openssl_pkcs12 - allow to specify certificate bundles in ``other_certificates`` by using new option ``other_certificates_parse_all`` (https://github.com/ansible-collections/community.crypto/issues/149, https://github.com/ansible-collections/community.crypto/pull/166).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_certificate - error when requested challenge type is not found for non-valid challenges, instead of hanging on step 2 (https://github.com/ansible-collections/community.crypto/issues/171, https://github.com/ansible-collections/community.crypto/pull/173).
|
||||
|
||||
v1.3.0
|
||||
======
|
||||
|
||||
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_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).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_pkcs12 - report the correct state when ``action`` is ``parse`` (https://github.com/ansible-collections/community.crypto/issues/143).
|
||||
- support code - improve handling of certificate and certificate signing request (CSR) loading with the ``cryptography`` backend when errors occur (https://github.com/ansible-collections/community.crypto/issues/138, https://github.com/ansible-collections/community.crypto/pull/139).
|
||||
- x509_certificate - fix ``entrust`` provider, which was broken since community.crypto 0.1.0 due to a feature added before the collection move (https://github.com/ansible-collections/community.crypto/pull/135).
|
||||
|
||||
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
|
||||
|
||||
v1.2.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Please note that this release fixes a security issue (CVE-2020-25646).
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_certificate - allow to pass CSR file as content with new option ``csr_content`` (https://github.com/ansible-collections/community.crypto/pull/115).
|
||||
- x509_certificate_info - add ``fingerprints`` return value which returns certificate fingerprints (https://github.com/ansible-collections/community.crypto/pull/121).
|
||||
|
||||
Security Fixes
|
||||
--------------
|
||||
|
||||
- openssl_csr - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- openssl_privatekey_info - the option ``content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- openssl_publickey - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- openssl_signature - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- x509_certificate - the options ``privatekey_content`` and ``ownca_privatekey_content`` were not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- x509_crl - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_pkcs12 - do not crash when reading PKCS#12 file which has no private key and/or no main certificate (https://github.com/ansible-collections/community.crypto/issues/103).
|
||||
|
||||
v1.1.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfixes for Ansible 2.10.0.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- meta/runtime.yml - convert Ansible version numbers for old names of modules to collection version numbers (https://github.com/ansible-collections/community.crypto/pull/108).
|
||||
- openssl_csr - improve handling of IDNA errors (https://github.com/ansible-collections/community.crypto/issues/105).
|
||||
|
||||
v1.1.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Release for Ansible 2.10.0.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_account - add ``external_account_binding`` option to allow creation of ACME accounts with External Account Binding (https://github.com/ansible-collections/community.crypto/issues/89).
|
||||
- acme_certificate - allow new selector ``test_certificates: first`` for ``select_chain`` parameter (https://github.com/ansible-collections/community.crypto/pull/102).
|
||||
- cryptography backends - support arbitrary dotted OIDs (https://github.com/ansible-collections/community.crypto/issues/39).
|
||||
- get_certificate - add support for SNI (https://github.com/ansible-collections/community.crypto/issues/69).
|
||||
- luks_device - add support for encryption options on container creation (https://github.com/ansible-collections/community.crypto/pull/97).
|
||||
- openssh_cert - add support for PKCS#11 tokens (https://github.com/ansible-collections/community.crypto/pull/95).
|
||||
- openssl_certificate - the PyOpenSSL backend now uses 160 bits of randomness for serial numbers, instead of a random number between 1000 and 99999. Please note that this is not a high quality random number (https://github.com/ansible-collections/community.crypto/issues/76).
|
||||
- openssl_csr - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46).
|
||||
- openssl_csr_info - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_inspect - fix problem with Python 3.5 that JSON was not decoded (https://github.com/ansible-collections/community.crypto/issues/86).
|
||||
- get_certificate - fix ``ca_cert`` option handling when ``proxy_host`` is used (https://github.com/ansible-collections/community.crypto/pull/84).
|
||||
- openssl_*, x509_* modules - fix handling of general names which refer to IP networks and not IP addresses (https://github.com/ansible-collections/community.crypto/pull/92).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- openssl_signature - Sign data with openssl
|
||||
- openssl_signature_info - Verify signatures with openssl
|
||||
|
||||
v1.0.0
|
||||
======
|
||||
@@ -13,7 +679,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
|
||||
-------------
|
||||
|
||||
|
||||
48
PSF-license.txt
Normal file
48
PSF-license.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
27
README.md
27
README.md
@@ -1,12 +1,29 @@
|
||||
# Ansible Community Crypto Collection
|
||||
[](https://app.shippable.com/projects/5e66776ca27f990007073a42)
|
||||
|
||||
[](https://codecov.io/gh/ansible-collections/community.crypto)
|
||||
|
||||
Provides modules for [Ansible](https://www.ansible.com/community) for various cryptographic operations.
|
||||
|
||||
You can find [documentation for this collection on the Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/).
|
||||
|
||||
Please note that this collection does **not** support Windows targets.
|
||||
|
||||
## Communication
|
||||
|
||||
* Join the Ansible forum:
|
||||
* [Get Help](https://forum.ansible.com/c/help/6): get help or help others. Please add appropriate tags if you start new discussions, for example the `crypto` or `acme` tags.
|
||||
* [Posts tagged with 'crypto'](https://forum.ansible.com/tag/crypto): subscribe to participate in cryptography related conversations.
|
||||
* [Posts tagged with 'acme'](https://forum.ansible.com/tag/acme): subscribe to participate in ACME (RFC 8555) related conversations.
|
||||
* [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts.
|
||||
* [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events.
|
||||
|
||||
* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes.
|
||||
|
||||
For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html).
|
||||
|
||||
## Tested with Ansible
|
||||
|
||||
Tested with both the current Ansible 2.9 and 2.10 releases and the current development version of Ansible. Ansible versions before 2.9.10 are not supported.
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12 and ansible-core 2.13 releases. Ansible versions before 2.9.10 are not supported.
|
||||
|
||||
## External requirements
|
||||
|
||||
@@ -24,6 +41,8 @@ Most modules require a recent enough version of [the Python cryptography library
|
||||
- openssl_privatekey_info
|
||||
- openssl_privatekey
|
||||
- openssl_publickey
|
||||
- openssl_signature_info
|
||||
- openssl_signature
|
||||
- x509_certificate_info
|
||||
- x509_certificate
|
||||
- x509_crl_info
|
||||
@@ -46,6 +65,8 @@ Most modules require a recent enough version of [the Python cryptography library
|
||||
- get_certificate
|
||||
- luks_device
|
||||
|
||||
You can also find a list of all modules with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/).
|
||||
|
||||
## Using this collection
|
||||
|
||||
Before using the crypto community collection, you need to install the collection with the `ansible-galaxy` CLI:
|
||||
@@ -76,7 +97,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/stable-1/CHANGELOG.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@@ -140,3 +140,740 @@ releases:
|
||||
name: x509_crl_info
|
||||
namespace: ''
|
||||
release_date: '2020-07-03'
|
||||
1.1.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_inspect - fix problem with Python 3.5 that JSON was not decoded (https://github.com/ansible-collections/community.crypto/issues/86).
|
||||
- get_certificate - fix ``ca_cert`` option handling when ``proxy_host`` is used
|
||||
(https://github.com/ansible-collections/community.crypto/pull/84).
|
||||
- openssl_*, x509_* modules - fix handling of general names which refer to IP
|
||||
networks and not IP addresses (https://github.com/ansible-collections/community.crypto/pull/92).
|
||||
minor_changes:
|
||||
- acme_account - add ``external_account_binding`` option to allow creation of
|
||||
ACME accounts with External Account Binding (https://github.com/ansible-collections/community.crypto/issues/89).
|
||||
- 'acme_certificate - allow new selector ``test_certificates: first`` for ``select_chain``
|
||||
parameter (https://github.com/ansible-collections/community.crypto/pull/102).'
|
||||
- cryptography backends - support arbitrary dotted OIDs (https://github.com/ansible-collections/community.crypto/issues/39).
|
||||
- get_certificate - add support for SNI (https://github.com/ansible-collections/community.crypto/issues/69).
|
||||
- luks_device - add support for encryption options on container creation (https://github.com/ansible-collections/community.crypto/pull/97).
|
||||
- openssh_cert - add support for PKCS#11 tokens (https://github.com/ansible-collections/community.crypto/pull/95).
|
||||
- openssl_certificate - the PyOpenSSL backend now uses 160 bits of randomness
|
||||
for serial numbers, instead of a random number between 1000 and 99999. Please
|
||||
note that this is not a high quality random number (https://github.com/ansible-collections/community.crypto/issues/76).
|
||||
- openssl_csr - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46).
|
||||
- openssl_csr_info - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46).
|
||||
release_summary: 'Release for Ansible 2.10.0.
|
||||
|
||||
'
|
||||
fragments:
|
||||
- 1.1.0.yml
|
||||
- 100-acme-account-external-account-binding.yml
|
||||
- 102-acme-certificate-select-chain-first.yml
|
||||
- 87-acme_inspect-python-3.5.yml
|
||||
- 90-cryptography-oids.yml
|
||||
- 90-openssl_certificate-pyopenssl-serial.yml
|
||||
- 92-ip-networks.yml
|
||||
- 92-openssl_csr-name-constraints.yml
|
||||
- get_certificate-add_support_for_SNI.yml
|
||||
- luks_device-add_encryption_option_on_create.yml
|
||||
- openssh_cert-pkcs11.yml
|
||||
modules:
|
||||
- description: Sign data with openssl
|
||||
name: openssl_signature
|
||||
namespace: ''
|
||||
- description: Verify signatures with openssl
|
||||
name: openssl_signature_info
|
||||
namespace: ''
|
||||
release_date: '2020-08-18'
|
||||
1.1.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- meta/runtime.yml - convert Ansible version numbers for old names of modules
|
||||
to collection version numbers (https://github.com/ansible-collections/community.crypto/pull/108).
|
||||
- openssl_csr - improve handling of IDNA errors (https://github.com/ansible-collections/community.crypto/issues/105).
|
||||
release_summary: Bugfixes for Ansible 2.10.0.
|
||||
fragments:
|
||||
- 1.1.1.yml
|
||||
- 106-openssl_csr-idna-errors.yml
|
||||
- 108-meta-runtime-versions.yml
|
||||
release_date: '2020-09-14'
|
||||
1.2.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_pkcs12 - do not crash when reading PKCS#12 file which has no private
|
||||
key and/or no main certificate (https://github.com/ansible-collections/community.crypto/issues/103).
|
||||
minor_changes:
|
||||
- acme_certificate - allow to pass CSR file as content with new option ``csr_content``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/115).
|
||||
- x509_certificate_info - add ``fingerprints`` return value which returns certificate
|
||||
fingerprints (https://github.com/ansible-collections/community.crypto/pull/121).
|
||||
release_summary: Please note that this release fixes a security issue (CVE-2020-25646).
|
||||
security_fixes:
|
||||
- openssl_csr - the option ``privatekey_content`` was not marked as ``no_log``,
|
||||
resulting in it being dumped into the system log by default, and returned
|
||||
in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- openssl_privatekey_info - the option ``content`` was not marked as ``no_log``,
|
||||
resulting in it being dumped into the system log by default, and returned
|
||||
in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- openssl_publickey - the option ``privatekey_content`` was not marked as ``no_log``,
|
||||
resulting in it being dumped into the system log by default, and returned
|
||||
in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- openssl_signature - the option ``privatekey_content`` was not marked as ``no_log``,
|
||||
resulting in it being dumped into the system log by default, and returned
|
||||
in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- x509_certificate - the options ``privatekey_content`` and ``ownca_privatekey_content``
|
||||
were not marked as ``no_log``, resulting in it being dumped into the system
|
||||
log by default, and returned in the registered results in the ``invocation``
|
||||
field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
- x509_crl - the option ``privatekey_content`` was not marked as ``no_log``,
|
||||
resulting in it being dumped into the system log by default, and returned
|
||||
in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125).
|
||||
fragments:
|
||||
- 1.2.0.yml
|
||||
- 109-openssl_pkcs12-crash-no-cert-key.yml
|
||||
- 115-acme_certificate-csr_content.yml
|
||||
- 121-x509_certificate_info-fingerprints.yml
|
||||
- cve-2020-25646.yml
|
||||
release_date: '2020-10-13'
|
||||
1.3.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_pkcs12 - report the correct state when ``action`` is ``parse`` (https://github.com/ansible-collections/community.crypto/issues/143).
|
||||
- support code - improve handling of certificate and certificate signing request
|
||||
(CSR) loading with the ``cryptography`` backend when errors occur (https://github.com/ansible-collections/community.crypto/issues/138,
|
||||
https://github.com/ansible-collections/community.crypto/pull/139).
|
||||
- x509_certificate - fix ``entrust`` provider, which was broken since community.crypto
|
||||
0.1.0 due to a feature added before the collection move (https://github.com/ansible-collections/community.crypto/pull/135).
|
||||
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_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).
|
||||
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.
|
||||
|
||||
'
|
||||
fragments:
|
||||
- 1.3.0.yml
|
||||
- 117-openssh_cert-use-ssh-agent.yml
|
||||
- 129-x509_certificate-no-csr-selfsigned.yml
|
||||
- 132-openssl_privatekey-ecc-order.yml
|
||||
- 135-x509_certificate-entrust.yml
|
||||
- 139-improve-error-handling.yml
|
||||
- 145-add-check-for-parsed-pkcs12-files.yml
|
||||
- privatekey-csr-certificate-refactoring.yml
|
||||
modules:
|
||||
- description: Generate OpenSSL Certificate Signing Request (CSR)
|
||||
name: openssl_csr_pipe
|
||||
namespace: ''
|
||||
- description: Generate OpenSSL private keys without disk access
|
||||
name: openssl_privatekey_pipe
|
||||
namespace: ''
|
||||
- description: Generate and/or check OpenSSL certificates
|
||||
name: x509_certificate_pipe
|
||||
namespace: ''
|
||||
release_date: '2020-11-24'
|
||||
1.4.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_certificate - error when requested challenge type is not found for non-valid
|
||||
challenges, instead of hanging on step 2 (https://github.com/ansible-collections/community.crypto/issues/171,
|
||||
https://github.com/ansible-collections/community.crypto/pull/173).
|
||||
minor_changes:
|
||||
- The ACME module_utils has been relicensed back from the Simplified BSD License
|
||||
(https://opensource.org/licenses/BSD-2-Clause) to the GPLv3+ (same license
|
||||
used by most other code in this collection). This undoes a licensing change
|
||||
when the original GPLv3+ licensed code was moved to module_utils in https://github.com/ansible/ansible/pull/40697
|
||||
(https://github.com/ansible-collections/community.crypto/pull/165).
|
||||
- The ``crypto/identify.py`` module_utils has been renamed to ``crypto/pem.py``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/166).
|
||||
- luks_device - ``new_keyfile``, ``new_passphrase``, ``remove_keyfile`` and
|
||||
``remove_passphrase`` are now idempotent (https://github.com/ansible-collections/community.crypto/issues/19,
|
||||
https://github.com/ansible-collections/community.crypto/pull/168).
|
||||
- luks_device - allow to configure PBKDF (https://github.com/ansible-collections/community.crypto/pull/163).
|
||||
- openssl_csr, openssl_csr_pipe - allow to specify CRL distribution endpoints
|
||||
with ``crl_distribution_points`` (https://github.com/ansible-collections/community.crypto/issues/147,
|
||||
https://github.com/ansible-collections/community.crypto/pull/167).
|
||||
- openssl_pkcs12 - allow to specify certificate bundles in ``other_certificates``
|
||||
by using new option ``other_certificates_parse_all`` (https://github.com/ansible-collections/community.crypto/issues/149,
|
||||
https://github.com/ansible-collections/community.crypto/pull/166).
|
||||
release_summary: Release with several new features and bugfixes.
|
||||
fragments:
|
||||
- 1.4.0.yml
|
||||
- 163-luks-pbkdf.yml
|
||||
- 166-openssl_pkcs12-certificate-bundles.yml
|
||||
- 167-openssl_csr-crl-distribution-points.yml
|
||||
- 168-luks_device-add-remove-idempotence.yml
|
||||
- 173-acme_certificate-wrong-challenge.yml
|
||||
- acme-module-utils-relicense.yml
|
||||
release_date: '2021-01-26'
|
||||
1.5.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_csr - no longer fails when comparing CSR without basic constraint
|
||||
when ``basic_constraints`` is specified (https://github.com/ansible-collections/community.crypto/issues/179,
|
||||
https://github.com/ansible-collections/community.crypto/pull/180).
|
||||
deprecated_features:
|
||||
- acme_account_info - when ``retrieve_orders=url_list``, ``orders`` will no
|
||||
longer be returned in community.crypto 2.0.0. Use ``order_uris`` instead (https://github.com/ansible-collections/community.crypto/pull/178).
|
||||
minor_changes:
|
||||
- acme_account_info - when ``retrieve_orders`` is not ``ignore`` and the ACME
|
||||
server allows to query orders, the new return value ``order_uris`` is always
|
||||
populated with a list of URIs (https://github.com/ansible-collections/community.crypto/pull/178).
|
||||
- luks_device - allow to specify sector size for LUKS2 containers with new ``sector_size``
|
||||
parameter (https://github.com/ansible-collections/community.crypto/pull/193).
|
||||
release_summary: Regular feature and bugfix release. Deprecates a return value.
|
||||
fragments:
|
||||
- 1.5.0.yml
|
||||
- 178-acme_account_info-orders-urls.yml
|
||||
- 179-openssl-csr-basic-constraint.yml
|
||||
- 193-luks_device-sector_size.yml
|
||||
release_date: '2021-03-08'
|
||||
1.6.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- action_module plugin helper - make compatible with latest changes in ansible-core
|
||||
2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202).
|
||||
- openssl_privatekey_pipe - make compatible with latest changes in ansible-core
|
||||
2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202).
|
||||
deprecated_features:
|
||||
- acme module_utils - the ``acme`` module_utils (``ansible_collections.community.crypto.plugins.module_utils.acme``)
|
||||
is deprecated and will be removed in community.crypto 2.0.0. Use the new Python
|
||||
modules in the ``acme`` package instead (``ansible_collections.community.crypto.plugins.module_utils.acme.xxx``)
|
||||
(https://github.com/ansible-collections/community.crypto/pull/184).
|
||||
minor_changes:
|
||||
- acme module_utils - the ``acme`` module_utils has been split up into several
|
||||
Python modules (https://github.com/ansible-collections/community.crypto/pull/184).
|
||||
- acme_* modules - codebase refactor which should not be visible to end-users
|
||||
(https://github.com/ansible-collections/community.crypto/pull/184).
|
||||
- acme_* modules - support account key passphrases for ``cryptography`` backend
|
||||
(https://github.com/ansible-collections/community.crypto/issues/197, https://github.com/ansible-collections/community.crypto/pull/207).
|
||||
- acme_certificate_revoke - support revoking by private keys that are passphrase
|
||||
protected for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/pull/207).
|
||||
- acme_challenge_cert_helper - add ``private_key_passphrase`` parameter (https://github.com/ansible-collections/community.crypto/pull/207).
|
||||
release_summary: Fixes compatibility issues with the latest ansible-core 2.11
|
||||
beta, and contains a lot of internal refactoring for the ACME modules and
|
||||
support for private key passphrases for them.
|
||||
fragments:
|
||||
- 1.6.0.yml
|
||||
- 184-acme-refactor.yml
|
||||
- 202-actionmodule-plugin-utils-ansible-core-2.11.yml
|
||||
- 207-acme-account-key-passphrase.yml
|
||||
release_date: '2021-03-22'
|
||||
1.6.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_* modules - fix wrong usages of ``ACMEProtocolException`` (https://github.com/ansible-collections/community.crypto/pull/216,
|
||||
https://github.com/ansible-collections/community.crypto/pull/217).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.6.1.yml
|
||||
- 217-acme-exceptions.yml
|
||||
release_date: '2021-04-11'
|
||||
1.6.2:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_* modules - avoid crashing for ACME servers where the ``meta`` directory
|
||||
key is not present (https://github.com/ansible-collections/community.crypto/issues/220,
|
||||
https://github.com/ansible-collections/community.crypto/pull/221).
|
||||
release_summary: Bugfix release. Fixes compatibility issue of ACME modules with
|
||||
step-ca.
|
||||
fragments:
|
||||
- 1.6.2.yml
|
||||
- 221-acme-meta.yml
|
||||
release_date: '2021-04-28'
|
||||
1.7.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_keypair - fix ``check_mode`` to populate return values for existing
|
||||
keypairs (https://github.com/ansible-collections/community.crypto/issues/113,
|
||||
https://github.com/ansible-collections/community.crypto/pull/230).
|
||||
- various modules - prevent crashes when modules try to set attributes on not
|
||||
yet existing files in check mode. This will be fixed in ansible-core 2.12,
|
||||
but it is not backported to every Ansible version we support (https://github.com/ansible-collections/community.crypto/issue/242,
|
||||
https://github.com/ansible-collections/community.crypto/pull/243).
|
||||
- x509_certificate - fix crash when ``assertonly`` provider is used and some
|
||||
error conditions should be reported (https://github.com/ansible-collections/community.crypto/issues/240,
|
||||
https://github.com/ansible-collections/community.crypto/pull/241).
|
||||
minor_changes:
|
||||
- cryptography_openssh module utils - new module_utils for managing asymmetric
|
||||
keypairs and OpenSSH formatted/encoded asymmetric keypairs (https://github.com/ansible-collections/community.crypto/pull/213).
|
||||
- openssh_keypair - added ``backend`` parameter for selecting between the cryptography
|
||||
library or the OpenSSH binary for the execution of actions performed by ``openssh_keypair``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/236).
|
||||
- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting
|
||||
OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).
|
||||
- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/233).
|
||||
- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
|
||||
- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography``
|
||||
backend. This requires cryptography 3.0 or newer, and does not support the
|
||||
``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234).
|
||||
- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_privatekey_info - refactor module to allow code re-use for diff mode
|
||||
(https://github.com/ansible-collections/community.crypto/pull/205).
|
||||
- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/233).
|
||||
- x509_certificate_info - refactor module to allow code re-use for diff mode
|
||||
(https://github.com/ansible-collections/community.crypto/pull/206).
|
||||
- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||
- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating
|
||||
all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232).
|
||||
- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
|
||||
release_summary: Regular feature and bugfix release.
|
||||
fragments:
|
||||
- 1.7.0.yml
|
||||
- 150-diff.yml
|
||||
- 203-x509_crl_info.yml
|
||||
- 204-openssl_csr_info.yml
|
||||
- 205-openssl_privatekey_info.yml
|
||||
- 206-x509_certificate_info.yml
|
||||
- 213-cryptography-openssh-module-utils.yml
|
||||
- 225-openssh-keypair-passphrase.yml
|
||||
- 230-openssh_keypair-check_mode-return-values.yml
|
||||
- 232-x509_crl_info-list_revoked_certificates.yml
|
||||
- 233-public-key-info.yml
|
||||
- 234-openssl_pkcs12-cryptography.yml
|
||||
- 236-openssh_keypair-backends.yml
|
||||
- 241-x509_certificate-assertonly.yml
|
||||
- 243-permission-check-crash.yml
|
||||
modules:
|
||||
- description: Provide information for OpenSSL public keys
|
||||
name: openssl_publickey_info
|
||||
namespace: ''
|
||||
release_date: '2021-06-02'
|
||||
1.7.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files
|
||||
with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247,
|
||||
https://github.com/ansible-collections/community.crypto/pull/248).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.7.1.yml
|
||||
- 248-openssl_pkcs12-passphrase-fix.yml
|
||||
release_date: '2021-06-11'
|
||||
1.8.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_cert - fixed certificate generation to restore original certificate
|
||||
if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
- openssh_keypair - fixed a bug that prevented custom file attributes being
|
||||
applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||
minor_changes:
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API
|
||||
available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates
|
||||
(https://github.com/ansible-collections/community.crypto/pull/246).
|
||||
- openssh_cert - added ``regenerate`` option to validate additional certificate
|
||||
parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 1.8.0.yml
|
||||
- 246-openssh-certificate-module-utils.yml
|
||||
- 255-openssh_cert-adding-diff-support.yml
|
||||
- 256-openssh_cert-adding-idempotency-option.yml
|
||||
- 257-openssh-keypair-fix-pubkey-permissions.yml
|
||||
- ansible-core-_text.yml
|
||||
release_date: '2021-08-10'
|
||||
1.9.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file
|
||||
permissions when regenerating a keypair requires existing files to be overwritten
|
||||
(https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration
|
||||
fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
minor_changes:
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from
|
||||
servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
release_summary: Regular feature release.
|
||||
fragments:
|
||||
- 1.9.0.yml
|
||||
- 260-openssh_keypair-diff-support.yml
|
||||
- 263-sanity.yml
|
||||
- 264-get_certificate-add-starttls-option.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.1:
|
||||
changes:
|
||||
release_summary: Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
release_date: '2021-08-30'
|
||||
1.9.10:
|
||||
changes:
|
||||
bugfixes:
|
||||
- luks_devices - set ``LANG`` and similar environment variables to avoid translated
|
||||
output, which can break some of the module's functionality like key management
|
||||
(https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.10.yml
|
||||
- 388-luks_device-i18n.yml
|
||||
release_date: '2022-02-01'
|
||||
1.9.11:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_cert - fixed false ``changed`` status for ``host`` certificates when
|
||||
using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395,
|
||||
https://github.com/ansible-collections/community.crypto/pull/396).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.11.yml
|
||||
- 396-openssh_cert-host-cert-idempotence-fix.yml
|
||||
release_date: '2022-02-05'
|
||||
1.9.12:
|
||||
changes:
|
||||
bugfixes:
|
||||
- certificate_complete_chain - allow multiple potential intermediate certificates
|
||||
to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399,
|
||||
https://github.com/ansible-collections/community.crypto/pull/403).
|
||||
- x509_certificate - for the ``ownca`` provider, check whether the CA private
|
||||
key actually belongs to the CA certificate. This fix only covers the ``cryptography``
|
||||
backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's public key changes
|
||||
for ``provider=ownca``. This fix only covers the ``cryptography`` backend,
|
||||
not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's subject changes for
|
||||
``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400,
|
||||
https://github.com/ansible-collections/community.crypto/pull/402).
|
||||
- x509_certificate - regenerate certificate when the private key changes for
|
||||
``provider=selfsigned``. This fix only covers the ``cryptography`` backend,
|
||||
not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
known_issues:
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl``
|
||||
backend, changing the CA's public key does not cause regeneration of the certificate
|
||||
(https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl``
|
||||
backend, it is possible to specify a CA private key which is not related to
|
||||
the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``selfsigned`` provider with the ``pyopenssl``
|
||||
backend, changing the private key does not cause regeneration of the certificate
|
||||
(https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.12.yml
|
||||
- 402-x509_certificate-ownca-subject.yml
|
||||
- 403-certificate_complete_chain-same-subject.yml
|
||||
- 407-x509_certificate-signature.yml
|
||||
release_date: '2022-02-21'
|
||||
1.9.13:
|
||||
changes:
|
||||
bugfixes:
|
||||
- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt``
|
||||
(https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.13.yml
|
||||
- 410-luks_device-lsblk-parsing.yml
|
||||
release_date: '2022-03-04'
|
||||
1.9.14:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Make collection more robust when PyOpenSSL is used with an incompatible cryptography
|
||||
version (https://github.com/ansible-collections/community.crypto/pull/446).
|
||||
- openssh_* modules - fix exception handling to report traceback to users for
|
||||
enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417).
|
||||
- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified
|
||||
(https://github.com/ansible-collections/community.crypto/pull/441).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.14.yml
|
||||
- 417-openssh_modules-fix-exception-reporting.yml
|
||||
- 441-x509-crl-cert-issuer.yml
|
||||
- 446-fix.yml
|
||||
release_date: '2022-05-09'
|
||||
1.9.15:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``.
|
||||
release_summary: Maintenance release.
|
||||
fragments:
|
||||
- 1.9.15.yml
|
||||
- psf-license.yml
|
||||
release_date: '2022-05-16'
|
||||
1.9.16:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Include ``simplified_bsd.txt`` license file for the ECS module utils.
|
||||
- certificate_complete_chain - do not stop execution if an unsupported signature
|
||||
algorithm is encountered; warn instead (https://github.com/ansible-collections/community.crypto/pull/457).
|
||||
release_summary: Maintenance and bugfix release.
|
||||
fragments:
|
||||
- 1.9.16.yml
|
||||
- 457-certificate_complete_chain-unsupported-algorithm.yml
|
||||
- simplified-bsd-license.yml
|
||||
release_date: '2022-06-02'
|
||||
1.9.17:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Include ``Apache-2.0.txt`` file for ``plugins/module_utils/crypto/_obj2txt.py``
|
||||
and ``plugins/module_utils/crypto/_objects_data.py``.
|
||||
- openssl_csr - the module no longer crashes with 'permitted_subtrees/excluded_subtrees
|
||||
must be a non-empty list or None' if only one of ``name_constraints_permitted``
|
||||
and ``name_constraints_excluded`` is provided (https://github.com/ansible-collections/community.crypto/issues/481).
|
||||
- x509_crl - do not crash when signing CRL with Ed25519 or Ed448 keys (https://github.com/ansible-collections/community.crypto/issues/473,
|
||||
https://github.com/ansible-collections/community.crypto/pull/474).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.17.yml
|
||||
- 474-x509_crl-ed25519-ed448.yml
|
||||
- 481-fix-excluded_subtrees-must-be-a-non-empty-list-or-None.yml
|
||||
- apache-license.yml
|
||||
release_date: '2022-06-17'
|
||||
1.9.18:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_pkcs12 - when using the pyOpenSSL backend, do not crash when trying
|
||||
to read non-existing other certificates (https://github.com/ansible-collections/community.crypto/issues/486,
|
||||
https://github.com/ansible-collections/community.crypto/pull/487).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.18.yml
|
||||
- 487-openssl_pkcs12-other-certs-crash.yml
|
||||
release_date: '2022-07-09'
|
||||
1.9.19:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_privatekey_pipe - ensure compatibility with newer versions of ansible-core
|
||||
(https://github.com/ansible-collections/community.crypto/pull/515).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.19.yml
|
||||
- 515-action-module-compat.yml
|
||||
release_date: '2022-11-01'
|
||||
1.9.2:
|
||||
changes:
|
||||
release_summary: Bugfix release to fix the changelog. No other change compared
|
||||
to 1.9.0.
|
||||
fragments:
|
||||
- 1.9.2.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.20:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_publickey_info - do not crash with internal error when public key
|
||||
cannot be parsed (https://github.com/ansible-collections/community.crypto/pull/551).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.20.yml
|
||||
- 551-publickey-info.yml
|
||||
release_date: '2023-01-01'
|
||||
1.9.21:
|
||||
changes:
|
||||
bugfixes:
|
||||
- action plugin helper - fix handling of deprecations for ansible-core 2.14.2
|
||||
(https://github.com/ansible-collections/community.crypto/pull/572).
|
||||
- openssl_csr, openssl_csr_pipe - prevent invalid values for ``crl_distribution_points``
|
||||
that do not have one of ``full_name``, ``relative_name``, and ``crl_issuer``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/560).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.21.yml
|
||||
- 560-openssl_csr-crl_distribution_points.yml
|
||||
- 572-action-module.yml
|
||||
release_date: '2023-04-16'
|
||||
1.9.22:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_keypair - always generate a new key pair if the private key does not
|
||||
exist. Previously, the module would fail when ``regenerate=fail`` without
|
||||
an existing key, contradicting the documentation (https://github.com/ansible-collections/community.crypto/pull/598).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.22.yml
|
||||
- 598-openssh_keypair-generate-new-key.yml
|
||||
release_date: '2023-06-15'
|
||||
1.9.23:
|
||||
changes:
|
||||
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).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.23.yml
|
||||
- pkcs12.yml
|
||||
release_date: '2023-10-29'
|
||||
1.9.24:
|
||||
changes:
|
||||
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).
|
||||
- x509_certificate - when using the PyOpenSSL backend with ``provider=assertonly``,
|
||||
better handle unexpected errors when validating private keys (https://github.com/ansible-collections/community.crypto/pull/704).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.24.yml
|
||||
- 698-openssl_dhparam-cryptography.yml
|
||||
- 701-private_key_info-consistency.yml
|
||||
- 704-x509_certificate-assertonly-privatekey.yml
|
||||
release_date: '2024-01-27'
|
||||
1.9.25:
|
||||
changes:
|
||||
bugfixes:
|
||||
- crypto.math module utils - change return values for ``quick_is_not_prime()``
|
||||
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).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.25.yml
|
||||
- 733-math-prime.yml
|
||||
- 740-ecs_certificate-renewal-without-csr.yml
|
||||
release_date: '2024-05-20'
|
||||
1.9.26:
|
||||
changes:
|
||||
major_changes:
|
||||
- 'The 1.x.y release train of community.crypto is **End of Life**. There will
|
||||
be no further community.crypto 1.x.y releases.
|
||||
|
||||
Please upgrade to community.crypto 2.x.y.
|
||||
|
||||
|
||||
Thanks to everyone who contributed to community.crypto 1.x.y!
|
||||
|
||||
'
|
||||
release_summary: Last release.
|
||||
fragments:
|
||||
- 0-readme.yml
|
||||
- eol.yml
|
||||
release_date: '2024-08-28'
|
||||
1.9.3:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used
|
||||
to compare strings with the cryptography backend. This fixes idempotency problems
|
||||
with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270,
|
||||
https://github.com/ansible-collections/community.crypto/pull/271).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.3.yml
|
||||
- 271-openssl_csr-utf8.yml
|
||||
release_date: '2021-09-14'
|
||||
1.9.4:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information
|
||||
on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``.
|
||||
This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||
- acme_challenge_cert_helper - only return exception when cryptography is not
|
||||
installed, not when a too old version of it is installed. This prevents Ansible's
|
||||
callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.4.yml
|
||||
- 279-acme-openssl.yml
|
||||
- 282-acme_challenge_cert_helper-error.yml
|
||||
release_date: '2021-09-28'
|
||||
1.9.5:
|
||||
changes:
|
||||
bugfixes:
|
||||
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||
in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||
in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
release_summary: Bugfix release to fully support cryptography 35.0.0.
|
||||
fragments:
|
||||
- 1.9.5.yml
|
||||
- 294-cryptography-35.0.0.yml
|
||||
- 296-openssl_pkcs12-cryptography-35.yml
|
||||
- 300-pyopenssl-cryptography-35.yml
|
||||
release_date: '2021-10-06'
|
||||
1.9.6:
|
||||
changes:
|
||||
bugfixes:
|
||||
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.6.yml
|
||||
- 313-unicode-names.yml
|
||||
release_date: '2021-10-30'
|
||||
1.9.7:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s
|
||||
X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code
|
||||
for extension parsing that works with cryptography 36.0.0 and newer. This
|
||||
code re-serializes de-serialized extensions and thus can return slightly different
|
||||
values if the extension in the original CSR resp. certificate was not canonicalized
|
||||
correctly. This code is currently used as a fallback if the existing code
|
||||
stops working, but we will switch it to be the main code in a future release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/331).
|
||||
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent``
|
||||
to make sure that also the secondary LUKS2 header is wiped when older versions
|
||||
of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326,
|
||||
https://github.com/ansible-collections/community.crypto/pull/327).
|
||||
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography
|
||||
36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||
minor_changes:
|
||||
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core
|
||||
``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||
release_summary: Bugfix release with extra forward compatibility for newer versions
|
||||
of cryptography.
|
||||
fragments:
|
||||
- 1.9.7.yml
|
||||
- 302-openssl_pkcs12-cryptography-36.0.0.yml
|
||||
- 324-acme_certificate-fullchain.yml
|
||||
- 327-luks_device-wipe.yml
|
||||
- 331-cryptography-extensions.yml
|
||||
- fetch_url-devel.yml
|
||||
release_date: '2021-11-22'
|
||||
1.9.8:
|
||||
changes:
|
||||
release_summary: Documentation fix release. No actual code changes.
|
||||
fragments:
|
||||
- 1.9.8.yml
|
||||
release_date: '2021-12-13'
|
||||
1.9.9:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Various modules and plugins - use vendored version of ``distutils.version``
|
||||
instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353).
|
||||
- certificate_complete_chain - do not append root twice if the chain already
|
||||
ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355,
|
||||
https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
fragments:
|
||||
- 353-distutils.version.yml
|
||||
- 360-certificate_complete_chain-loop.yml
|
||||
release_date: '2022-01-11'
|
||||
|
||||
@@ -6,6 +6,9 @@ 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:
|
||||
|
||||
6
docs/docsite/extra-docs.yml
Normal file
6
docs/docsite/extra-docs.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
sections:
|
||||
- title: Scenario Guides
|
||||
toctree:
|
||||
- guide_selfsigned
|
||||
- guide_ownca
|
||||
148
docs/docsite/rst/guide_ownca.rst
Normal file
148
docs/docsite/rst/guide_ownca.rst
Normal file
@@ -0,0 +1,148 @@
|
||||
.. _ansible_collections.community.crypto.docsite.guide_ownca:
|
||||
|
||||
How to create a small CA
|
||||
========================
|
||||
|
||||
The `community.crypto collection <https://galaxy.ansible.com/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.
|
||||
|
||||
Set up the CA
|
||||
-------------
|
||||
|
||||
Any certificate can be used as a CA certificate. You can create a self-signed certificate (see :ref:`ansible_collections.community.crypto.docsite.guide_selfsigned`), use another CA certificate to sign a new certificate (using the instructions below for signing a certificate), ask (and pay) a commercial CA to sign your CA certificate, etc.
|
||||
|
||||
The following instructions show how to set up a simple self-signed CA certificate.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create private key with password protection
|
||||
community.crypto.openssl_privatekey:
|
||||
path: /path/to/ca-certificate.key
|
||||
passphrase: "{{ secret_ca_passphrase }}"
|
||||
|
||||
- name: Create certificate signing request (CSR) for CA certificate
|
||||
community.crypto.openssl_csr_pipe:
|
||||
privatekey_path: /path/to/ca-certificate.key
|
||||
privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||
common_name: Ansible CA
|
||||
use_common_name_for_san: false # since we do not specify SANs, don't use CN as a SAN
|
||||
basic_constraints:
|
||||
- 'CA:TRUE'
|
||||
basic_constraints_critical: yes
|
||||
key_usage:
|
||||
- keyCertSign
|
||||
key_usage_critical: true
|
||||
register: ca_csr
|
||||
|
||||
- name: Create self-signed CA certificate from CSR
|
||||
community.crypto.x509_certificate:
|
||||
path: /path/to/ca-certificate.pem
|
||||
csr_content: "{{ ca_csr.csr }}"
|
||||
privatekey_path: /path/to/ca-certificate.key
|
||||
privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||
provider: selfsigned
|
||||
|
||||
Use the CA to sign a certificate
|
||||
--------------------------------
|
||||
|
||||
To sign a certificate, you must pass a CSR to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>` or :ref:`community.crypto.x509_certificate_pipe module <ansible_collections.community.crypto.x509_certificate_pipe_module>`.
|
||||
|
||||
In the following example, we assume that the certificate to sign (including its private key) are on ``server_1``, while our CA certificate is on ``server_2``. We do not want any key material to leave each respective server.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create private key for new certificate on server_1
|
||||
community.crypto.openssl_privatekey:
|
||||
path: /path/to/certificate.key
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
|
||||
- name: Create certificate signing request (CSR) for new certificate
|
||||
community.crypto.openssl_csr_pipe:
|
||||
privatekey_path: /path/to/certificate.key
|
||||
subject_alt_name:
|
||||
- "DNS:ansible.com"
|
||||
- "DNS:www.ansible.com"
|
||||
- "DNS:docs.ansible.com"
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
register: csr
|
||||
|
||||
- name: Sign certificate with our CA
|
||||
community.crypto.x509_certificate_pipe:
|
||||
csr_content: "{{ csr.csr }}"
|
||||
provider: ownca
|
||||
ownca_path: /path/to/ca-certificate.pem
|
||||
ownca_privatekey_path: /path/to/ca-certificate.key
|
||||
ownca_privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||
ownca_not_after: +365d # valid for one year
|
||||
ownca_not_before: "-1d" # valid since yesterday
|
||||
delegate_to: server_2
|
||||
run_once: true
|
||||
register: certificate
|
||||
|
||||
- name: Write certificate file on server_1
|
||||
copy:
|
||||
dest: /path/to/certificate.pem
|
||||
content: "{{ certificate.certificate }}"
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
|
||||
Please note that the above procedure is **not idempotent**. The following extended example reads the existing certificate from ``server_1`` (if exists) and provides it to the :ref:`community.crypto.x509_certificate_pipe module <ansible_collections.community.crypto.x509_certificate_pipe_module>`, and only writes the result back if it was changed:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create private key for new certificate on server_1
|
||||
community.crypto.openssl_privatekey:
|
||||
path: /path/to/certificate.key
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
|
||||
- name: Create certificate signing request (CSR) for new certificate
|
||||
community.crypto.openssl_csr_pipe:
|
||||
privatekey_path: /path/to/certificate.key
|
||||
subject_alt_name:
|
||||
- "DNS:ansible.com"
|
||||
- "DNS:www.ansible.com"
|
||||
- "DNS:docs.ansible.com"
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
register: csr
|
||||
|
||||
- name: Check whether certificate exists
|
||||
stat:
|
||||
path: /path/to/certificate.pem
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
register: certificate_exists
|
||||
|
||||
- name: Read existing certificate if exists
|
||||
slurp:
|
||||
src: /path/to/certificate.pem
|
||||
when: certificate_exists.stat.exists
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
register: certificate
|
||||
|
||||
- name: Sign certificate with our CA
|
||||
community.crypto.x509_certificate_pipe:
|
||||
content: "{{ (certificate.content | b64decode) if certificate_exists.stat.exists else omit }}"
|
||||
csr_content: "{{ csr.csr }}"
|
||||
provider: ownca
|
||||
ownca_path: /path/to/ca-certificate.pem
|
||||
ownca_privatekey_path: /path/to/ca-certificate.key
|
||||
ownca_privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||
ownca_not_after: +365d # valid for one year
|
||||
ownca_not_before: "-1d" # valid since yesterday
|
||||
delegate_to: server_2
|
||||
run_once: true
|
||||
register: certificate
|
||||
|
||||
- name: Write certificate file on server_1
|
||||
copy:
|
||||
dest: /path/to/certificate.pem
|
||||
content: "{{ certificate.certificate }}"
|
||||
delegate_to: server_1
|
||||
run_once: true
|
||||
when: certificate is changed
|
||||
60
docs/docsite/rst/guide_selfsigned.rst
Normal file
60
docs/docsite/rst/guide_selfsigned.rst
Normal file
@@ -0,0 +1,60 @@
|
||||
.. _ansible_collections.community.crypto.docsite.guide_selfsigned:
|
||||
|
||||
How to create self-signed certificates
|
||||
======================================
|
||||
|
||||
The `community.crypto collection <https://galaxy.ansible.com/ui/repo/published/community/crypto/>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates.
|
||||
|
||||
For creating any kind of certificate, you always have to start with a private key. You can use the :ref:`community.crypto.openssl_privatekey module <ansible_collections.community.crypto.openssl_privatekey_module>` to create a private key. If you only specify ``path``, the default parameters will be used. This will result in a 4096 bit RSA private key:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create private key (RSA, 4096 bits)
|
||||
community.crypto.openssl_privatekey:
|
||||
path: /path/to/certificate.key
|
||||
|
||||
You can specify ``type`` to select another key type, ``size`` to select a different key size (only available for RSA and DSA keys), or ``passphrase`` if you want to store the key password-protected:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create private key (X25519) with password protection
|
||||
community.crypto.openssl_privatekey:
|
||||
path: /path/to/certificate.key
|
||||
type: X25519
|
||||
passphrase: changeme
|
||||
|
||||
To create a very simple self-signed certificate with no specific information, you can proceed directly with the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create simple self-signed certificate
|
||||
community.crypto.x509_certificate:
|
||||
path: /path/to/certificate.pem
|
||||
privatekey_path: /path/to/certificate.key
|
||||
provider: selfsigned
|
||||
|
||||
(If you used ``passphrase`` for the private key, you have to provide ``privatekey_passphrase``.)
|
||||
|
||||
You can use ``selfsigned_not_after`` to define when the certificate expires (default: in roughly 10 years), and ``selfsigned_not_before`` to define from when the certificate is valid (default: now).
|
||||
|
||||
To define further properties of the certificate, like the subject, Subject Alternative Names (SANs), key usages, name constraints, etc., you need to first create a Certificate Signing Request (CSR) and provide it to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`. If you do not need the CSR file, you can use the :ref:`community.crypto.openssl_csr_pipe module <ansible_collections.community.crypto.openssl_csr_pipe_module>` as in the example below. (To store it to disk, use the :ref:`community.crypto.openssl_csr module <ansible_collections.community.crypto.openssl_csr_module>` instead.)
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create certificate signing request (CSR) for self-signed certificate
|
||||
community.crypto.openssl_csr_pipe:
|
||||
privatekey_path: /path/to/certificate.key
|
||||
common_name: ansible.com
|
||||
organization_name: Ansible, Inc.
|
||||
subject_alt_name:
|
||||
- "DNS:ansible.com"
|
||||
- "DNS:www.ansible.com"
|
||||
- "DNS:docs.ansible.com"
|
||||
register: csr
|
||||
|
||||
- name: Create self-signed certificate from CSR
|
||||
community.crypto.x509_certificate:
|
||||
path: /path/to/certificate.pem
|
||||
csr_content: "{{ csr.csr }}"
|
||||
privatekey_path: /path/to/certificate.key
|
||||
provider: selfsigned
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace: community
|
||||
name: crypto
|
||||
version: 1.0.0
|
||||
version: 1.9.26
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (github.com/ansible)
|
||||
@@ -21,7 +21,7 @@ tags:
|
||||
- openssh
|
||||
- pkcs12
|
||||
repository: https://github.com/ansible-collections/community.crypto
|
||||
#documentation: https://github.com/ansible-collection-migration/community.crypto/tree/main/docs
|
||||
documentation: https://docs.ansible.com/ansible/latest/collections/community/crypto/
|
||||
homepage: https://github.com/ansible-collections/community.crypto
|
||||
issues: https://github.com/ansible-collections/community.crypto/issues
|
||||
build_ignore:
|
||||
|
||||
@@ -13,13 +13,19 @@ plugin_routing:
|
||||
modules:
|
||||
acme_account_facts:
|
||||
deprecation:
|
||||
removal_version: '2.12'
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'.
|
||||
openssl_certificate:
|
||||
deprecation:
|
||||
removal_version: '2.14'
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'
|
||||
openssl_certificate_info:
|
||||
deprecation:
|
||||
removal_version: '2.14'
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'
|
||||
module_utils:
|
||||
crypto.identify:
|
||||
redirect: community.crypto.crypto.pem
|
||||
deprecation:
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'crypto/identify.py' module_utils has been renamed 'crypto/pem.py'. Please update your imports
|
||||
|
||||
107
plugins/action/openssl_privatekey_pipe.py
Normal file
107
plugins/action/openssl_privatekey_pipe.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import (
|
||||
select_backend,
|
||||
get_privatekey_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
class PrivateKeyModule(object):
|
||||
def __init__(self, module, module_backend):
|
||||
self.module = module
|
||||
self.module_backend = module_backend
|
||||
self.check_mode = module.check_mode
|
||||
self.changed = False
|
||||
self.return_current_key = module.params['return_current_key']
|
||||
|
||||
if module.params['content'] is not None:
|
||||
if module.params['content_base64']:
|
||||
try:
|
||||
data = base64.b64decode(module.params['content'])
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Cannot decode Base64 encoded data: {0}'.format(e))
|
||||
else:
|
||||
data = to_bytes(module.params['content'])
|
||||
module_backend.set_existing(data)
|
||||
|
||||
def generate(self, module):
|
||||
"""Generate a keypair."""
|
||||
|
||||
if self.module_backend.needs_regeneration():
|
||||
# Regenerate
|
||||
if not self.check_mode:
|
||||
self.module_backend.generate_private_key()
|
||||
privatekey_data = self.module_backend.get_private_key_data()
|
||||
self.privatekey_bytes = privatekey_data
|
||||
self.changed = True
|
||||
elif self.module_backend.needs_conversion():
|
||||
# Convert
|
||||
if not self.check_mode:
|
||||
self.module_backend.convert_private_key()
|
||||
privatekey_data = self.module_backend.get_private_key_data()
|
||||
self.privatekey_bytes = privatekey_data
|
||||
self.changed = True
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = self.module_backend.dump(include_key=self.changed or self.return_current_key)
|
||||
result['changed'] = self.changed
|
||||
return result
|
||||
|
||||
|
||||
class ActionModule(ActionModuleBase):
|
||||
@staticmethod
|
||||
def setup_module():
|
||||
argument_spec = get_privatekey_argument_spec()
|
||||
argument_spec.argument_spec.update(dict(
|
||||
content=dict(type='str', no_log=True),
|
||||
content_base64=dict(type='bool', default=False),
|
||||
return_current_key=dict(type='bool', default=False),
|
||||
))
|
||||
return argument_spec, dict(
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def run_module(module):
|
||||
backend, module_backend = select_backend(
|
||||
module=module,
|
||||
backend=module.params['select_crypto_backend'],
|
||||
)
|
||||
|
||||
try:
|
||||
private_key = PrivateKeyModule(module, module_backend)
|
||||
private_key.generate(module)
|
||||
result = private_key.dump()
|
||||
if private_key.return_current_key:
|
||||
# In case the module's input (`content`) is returned as `privatekey`:
|
||||
# Since `content` is no_log=True, `privatekey`'s value will get replaced by
|
||||
# VALUE_SPECIFIED_IN_NO_LOG_PARAMETER. To avoid this, we remove the value of
|
||||
# `content` from module.no_log_values. Since we explicitly set
|
||||
# `module.no_log = True`, this should be safe.
|
||||
module.no_log = True
|
||||
try:
|
||||
module.no_log_values.remove(module.params['content'])
|
||||
except KeyError:
|
||||
pass
|
||||
module.params['content'] = 'ANSIBLE_NO_LOG_VALUE'
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
@@ -31,8 +31,12 @@ options:
|
||||
description:
|
||||
- "Path to a file containing the ACME account RSA or Elliptic Curve
|
||||
key."
|
||||
- "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 can be created with the
|
||||
M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
|
||||
modules. If the requisites (pyOpenSSL or cryptography) are not available,
|
||||
keys can also be created directly with the C(openssl) command line tool:
|
||||
RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
|
||||
can be created with C(openssl ecparam -genkey ...). Any other tool creating
|
||||
private keys in PEM format can be used as well."
|
||||
- "Mutually exclusive with C(account_key_content)."
|
||||
- "Required if C(account_key_content) is not used."
|
||||
@@ -43,7 +47,7 @@ options:
|
||||
- "Content of the ACME account RSA or Elliptic Curve key."
|
||||
- "Mutually exclusive with C(account_key_src)."
|
||||
- "Required if C(account_key_src) is not used."
|
||||
- "*Warning:* the content will be written into a temporary file, which will
|
||||
- "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
|
||||
@@ -53,6 +57,12 @@ options:
|
||||
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
|
||||
@@ -62,40 +72,52 @@ options:
|
||||
acme_version:
|
||||
description:
|
||||
- "The ACME version of the endpoint."
|
||||
- "Must be 1 for the classic Let's Encrypt and Buypass ACME endpoints,
|
||||
or 2 for standardized ACME v2 endpoints."
|
||||
- "The default value is 1. Note that in Ansible 2.14, this option *will
|
||||
be required* and will no longer have a default."
|
||||
- "Must be C(1) for the classic Let's Encrypt and Buypass ACME endpoints,
|
||||
or C(2) for standardized ACME v2 endpoints."
|
||||
- "The default value is C(1). Note that in community.crypto 2.0.0, this
|
||||
option B(will be required) and will no longer have a default."
|
||||
- "Please also note that we will deprecate ACME v1 support eventually."
|
||||
type: int
|
||||
choices: [ 1, 2 ]
|
||||
acme_directory:
|
||||
description:
|
||||
- "The ACME directory to use. This is the entry point URL to access
|
||||
CA server API."
|
||||
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."
|
||||
- "The default value is U(https://acme-staging.api.letsencrypt.org/directory).
|
||||
Note that in Ansible 2.14, this option *will be required* and will no longer
|
||||
have a default."
|
||||
- "The default value is C(https://acme-staging.api.letsencrypt.org/directory).
|
||||
Note that in community.crypto 2.0.0, this option B(will be required) and
|
||||
will no longer have a default. Note that the default is the Let's Encrypt
|
||||
staging server for the ACME v1 protocol, which is deprecated and will
|
||||
be disabled in May 2021 (see
|
||||
L(here,https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430/7)
|
||||
for details)."
|
||||
- "For Let's Encrypt, all staging endpoints can be found here:
|
||||
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
|
||||
endpoints can be found here:
|
||||
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
|
||||
- "For Let's Encrypt, the production directory URL for ACME v1 is
|
||||
U(https://acme-v01.api.letsencrypt.org/directory), and the production
|
||||
directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory)."
|
||||
- "For Buypass, the production directory URL for ACME v2 and v1 is
|
||||
- "For B(Let's Encrypt), the production directory URL for ACME v2 is
|
||||
U(https://acme-v02.api.letsencrypt.org/directory).
|
||||
(The production directory URL for ACME v1 is
|
||||
U(https://acme-v01.api.letsencrypt.org/directory) and will be
|
||||
disabled in July 2021.)"
|
||||
- "For B(Buypass), the production directory URL for ACME v2 and v1 is
|
||||
U(https://api.buypass.com/acme/directory)."
|
||||
- "*Warning:* So far, the module has only been tested against Let's Encrypt
|
||||
(staging and production), Buypass (staging and production), and
|
||||
L(Pebble testing server,https://github.com/letsencrypt/Pebble)."
|
||||
- "For B(ZeroSSL), the production directory URL for ACME v2 is
|
||||
U(https://acme.zerossl.com/v2/DV90)."
|
||||
- "B(Warning:) So far, the ACME modules have only been tested against Let's Encrypt
|
||||
(staging and production), Buypass (staging and production), ZeroSSL (production),
|
||||
and L(Pebble testing server,https://github.com/letsencrypt/Pebble). If you
|
||||
experience problems with another ACME server, please
|
||||
L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
|
||||
to help us supporting it. Feedback that an ACME server not mentioned does work
|
||||
is also appreciated."
|
||||
type: str
|
||||
validate_certs:
|
||||
description:
|
||||
- Whether calls to the ACME directory will validate TLS certificates.
|
||||
- "*Warning:* Should *only ever* be set to C(no) for testing purposes,
|
||||
- "B(Warning:) Should B(only ever) be set to C(no) for testing purposes,
|
||||
for example when testing against a local Pebble server."
|
||||
type: bool
|
||||
default: yes
|
||||
|
||||
587
plugins/doc_fragments/module_certificate.py
Normal file
587
plugins/doc_fragments/module_certificate.py
Normal file
@@ -0,0 +1,587 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL certificates.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL.
|
||||
- If both the cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with C(select_crypto_backend)).
|
||||
Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned), C(ownca) or C(assertonly) provider)
|
||||
options:
|
||||
force:
|
||||
description:
|
||||
- Generate the certificate, even if it already exists.
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
csr_path:
|
||||
description:
|
||||
- Path to the Certificate Signing Request (CSR) used to generate this certificate.
|
||||
- This is mutually exclusive with I(csr_content).
|
||||
type: path
|
||||
csr_content:
|
||||
description:
|
||||
- Content of the Certificate Signing Request (CSR) used to generate this certificate.
|
||||
- This is mutually exclusive with I(csr_path).
|
||||
type: str
|
||||
|
||||
privatekey_path:
|
||||
description:
|
||||
- Path to the private key to use when signing the certificate.
|
||||
- This is mutually exclusive with I(privatekey_content).
|
||||
type: path
|
||||
privatekey_content:
|
||||
description:
|
||||
- Content of the private key to use when signing the certificate.
|
||||
- This is mutually exclusive with I(privatekey_path).
|
||||
type: str
|
||||
|
||||
privatekey_passphrase:
|
||||
description:
|
||||
- The passphrase for the I(privatekey_path) resp. I(privatekey_content).
|
||||
- This is required if the private key is password protected.
|
||||
type: str
|
||||
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
notes:
|
||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||
- Date specified should be UTC. Minutes and seconds are mandatory.
|
||||
- For security reason, when you use C(ownca) provider, you should NOT run
|
||||
M(community.crypto.x509_certificate) on a target machine, but on a dedicated CA machine. It
|
||||
is recommended not to store the CA private key on the target machine. Once signed, the
|
||||
certificate can be moved to the target machine.
|
||||
seealso:
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_csr_pipe
|
||||
- module: community.crypto.openssl_dhparam
|
||||
- module: community.crypto.openssl_pkcs12
|
||||
- module: community.crypto.openssl_privatekey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
- module: community.crypto.openssl_publickey
|
||||
'''
|
||||
|
||||
BACKEND_ACME_DOCUMENTATION = r'''
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL certificates.
|
||||
requirements:
|
||||
- acme-tiny >= 4.0.0 (if using the C(acme) provider)
|
||||
options:
|
||||
acme_accountkey_path:
|
||||
description:
|
||||
- The path to the accountkey for the C(acme) provider.
|
||||
- This is only used by the C(acme) provider.
|
||||
type: path
|
||||
|
||||
acme_challenge_path:
|
||||
description:
|
||||
- The path to the ACME challenge directory that is served on U(http://<HOST>:80/.well-known/acme-challenge/)
|
||||
- This is only used by the C(acme) provider.
|
||||
type: path
|
||||
|
||||
acme_chain:
|
||||
description:
|
||||
- Include the intermediate certificate to the generated certificate
|
||||
- This is only used by the C(acme) provider.
|
||||
- Note that this is only available for older versions of C(acme-tiny).
|
||||
New versions include the chain automatically, and setting I(acme_chain) to C(yes) results in an error.
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
acme_directory:
|
||||
description:
|
||||
- "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt."
|
||||
- "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)."
|
||||
type: str
|
||||
default: https://acme-v02.api.letsencrypt.org/directory
|
||||
'''
|
||||
|
||||
BACKEND_ASSERTONLY_DOCUMENTATION = r'''
|
||||
description:
|
||||
- The C(assertonly) provider is intended for use cases where one is only interested in
|
||||
checking properties of a supplied certificate. Please note that this provider has been
|
||||
deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0. See the examples on how
|
||||
to emulate C(assertonly) usage with M(community.crypto.x509_certificate_info),
|
||||
M(community.crypto.openssl_csr_info), M(community.crypto.openssl_privatekey_info) and
|
||||
M(ansible.builtin.assert). This also allows more flexible checks than
|
||||
the ones offered by the C(assertonly) provider.
|
||||
- Many properties that can be specified in this module are for validation of an
|
||||
existing or newly generated certificate. The proper place to specify them, if you
|
||||
want to receive a certificate with these properties is a CSR (Certificate Signing Request).
|
||||
options:
|
||||
csr_path:
|
||||
description:
|
||||
- This is not required for the C(assertonly) provider.
|
||||
|
||||
csr_content:
|
||||
description:
|
||||
- This is not required for the C(assertonly) provider.
|
||||
|
||||
signature_algorithms:
|
||||
description:
|
||||
- A list of algorithms that you would accept the certificate to be signed with
|
||||
(e.g. ['sha256WithRSAEncryption', 'sha512WithRSAEncryption']).
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
issuer:
|
||||
description:
|
||||
- The key/value pairs that must be present in the issuer name field of the certificate.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: dict
|
||||
|
||||
issuer_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(issuer) field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
subject:
|
||||
description:
|
||||
- The key/value pairs that must be present in the subject name field of the certificate.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: dict
|
||||
|
||||
subject_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(subject) field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
has_expired:
|
||||
description:
|
||||
- Checks if the certificate is expired/not expired at the time the module is executed.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
version:
|
||||
description:
|
||||
- The version of the certificate.
|
||||
- Nowadays it should almost always be 3.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: int
|
||||
|
||||
valid_at:
|
||||
description:
|
||||
- The certificate must be valid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
invalid_at:
|
||||
description:
|
||||
- The certificate must be invalid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
not_before:
|
||||
description:
|
||||
- The certificate must start to become valid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
aliases: [ notBefore ]
|
||||
|
||||
not_after:
|
||||
description:
|
||||
- The certificate must expire at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
aliases: [ notAfter ]
|
||||
|
||||
valid_in:
|
||||
description:
|
||||
- The certificate must still be valid at this relative time offset from now.
|
||||
- Valid format is C([+-]timespec | number_of_seconds) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- Note that if using this parameter, this module is NOT idempotent.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
key_usage:
|
||||
description:
|
||||
- The I(key_usage) extension field must contain all these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ keyUsage ]
|
||||
|
||||
key_usage_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(key_usage) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ keyUsage_strict ]
|
||||
|
||||
extended_key_usage:
|
||||
description:
|
||||
- The I(extended_key_usage) extension field must contain all these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ extendedKeyUsage ]
|
||||
|
||||
extended_key_usage_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(extended_key_usage) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ extendedKeyUsage_strict ]
|
||||
|
||||
subject_alt_name:
|
||||
description:
|
||||
- The I(subject_alt_name) extension field must contain these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ subjectAltName ]
|
||||
|
||||
subject_alt_name_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(subject_alt_name) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ subjectAltName_strict ]
|
||||
'''
|
||||
|
||||
BACKEND_ENTRUST_DOCUMENTATION = r'''
|
||||
options:
|
||||
entrust_cert_type:
|
||||
description:
|
||||
- Specify the type of certificate requested.
|
||||
- This is only used by the C(entrust) provider.
|
||||
type: str
|
||||
default: STANDARD_SSL
|
||||
choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
|
||||
|
||||
entrust_requester_email:
|
||||
description:
|
||||
- The email of the requester of the certificate (for tracking purposes).
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: str
|
||||
|
||||
entrust_requester_name:
|
||||
description:
|
||||
- The name of the requester of the certificate (for tracking purposes).
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: str
|
||||
|
||||
entrust_requester_phone:
|
||||
description:
|
||||
- The phone number of the requester of the certificate (for tracking purposes).
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: str
|
||||
|
||||
entrust_api_user:
|
||||
description:
|
||||
- The username for authentication to the Entrust Certificate Services (ECS) API.
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: str
|
||||
|
||||
entrust_api_key:
|
||||
description:
|
||||
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: str
|
||||
|
||||
entrust_api_client_cert_path:
|
||||
description:
|
||||
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: path
|
||||
|
||||
entrust_api_client_cert_key_path:
|
||||
description:
|
||||
- The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||
- This is only used by the C(entrust) provider.
|
||||
- This is required if the provider is C(entrust).
|
||||
type: path
|
||||
|
||||
entrust_not_after:
|
||||
description:
|
||||
- The point in time at which the certificate stops being valid.
|
||||
- Time can be specified either as relative time or as an absolute timestamp.
|
||||
- A valid absolute time format is C(ASN.1 TIME) such as C(2019-06-18).
|
||||
- A valid relative time format is C([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as C(+365d) or C(+32w1d2h)).
|
||||
- Time will always be interpreted as UTC.
|
||||
- Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate.
|
||||
- The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day
|
||||
earlier than expected if a relative time is used.
|
||||
- The minimum certificate lifetime is 90 days, and maximum is three years.
|
||||
- If this value is not specified, the certificate will stop being valid 365 days the date of issue.
|
||||
- This is only used by the C(entrust) provider.
|
||||
type: str
|
||||
default: +365d
|
||||
|
||||
entrust_api_specification_path:
|
||||
description:
|
||||
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
|
||||
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
|
||||
- This is only used by the C(entrust) provider.
|
||||
type: path
|
||||
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
|
||||
'''
|
||||
|
||||
BACKEND_OWNCA_DOCUMENTATION = r'''
|
||||
description:
|
||||
- The C(ownca) provider is intended for generating an OpenSSL certificate signed with your own
|
||||
CA (Certificate Authority) certificate (self-signed certificate).
|
||||
options:
|
||||
ownca_path:
|
||||
description:
|
||||
- Remote absolute path of the CA (Certificate Authority) certificate.
|
||||
- This is only used by the C(ownca) provider.
|
||||
- This is mutually exclusive with I(ownca_content).
|
||||
type: path
|
||||
ownca_content:
|
||||
description:
|
||||
- Content of the CA (Certificate Authority) certificate.
|
||||
- This is only used by the C(ownca) provider.
|
||||
- This is mutually exclusive with I(ownca_path).
|
||||
type: str
|
||||
|
||||
ownca_privatekey_path:
|
||||
description:
|
||||
- Path to the CA (Certificate Authority) private key to use when signing the certificate.
|
||||
- This is only used by the C(ownca) provider.
|
||||
- This is mutually exclusive with I(ownca_privatekey_content).
|
||||
type: path
|
||||
ownca_privatekey_content:
|
||||
description:
|
||||
- Content of the CA (Certificate Authority) private key to use when signing the certificate.
|
||||
- This is only used by the C(ownca) provider.
|
||||
- This is mutually exclusive with I(ownca_privatekey_path).
|
||||
type: str
|
||||
|
||||
ownca_privatekey_passphrase:
|
||||
description:
|
||||
- The passphrase for the I(ownca_privatekey_path) resp. I(ownca_privatekey_content).
|
||||
- This is only used by the C(ownca) provider.
|
||||
type: str
|
||||
|
||||
ownca_digest:
|
||||
description:
|
||||
- The digest algorithm to be used for the C(ownca) certificate.
|
||||
- This is only used by the C(ownca) provider.
|
||||
type: str
|
||||
default: sha256
|
||||
|
||||
ownca_version:
|
||||
description:
|
||||
- The version of the C(ownca) certificate.
|
||||
- Nowadays it should almost always be C(3).
|
||||
- This is only used by the C(ownca) provider.
|
||||
type: int
|
||||
default: 3
|
||||
|
||||
ownca_not_before:
|
||||
description:
|
||||
- The point in time the certificate is valid from.
|
||||
- Time can be specified either as relative time or as absolute timestamp.
|
||||
- Time will always be interpreted as UTC.
|
||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- If this value is not specified, the certificate will start being valid from now.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- This is only used by the C(ownca) provider.
|
||||
type: str
|
||||
default: +0s
|
||||
|
||||
ownca_not_after:
|
||||
description:
|
||||
- The point in time at which the certificate stops being valid.
|
||||
- Time can be specified either as relative time or as absolute timestamp.
|
||||
- Time will always be interpreted as UTC.
|
||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- This is only used by the C(ownca) provider.
|
||||
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
|
||||
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||
type: str
|
||||
default: +3650d
|
||||
|
||||
ownca_create_subject_key_identifier:
|
||||
description:
|
||||
- Whether to create the Subject Key Identifier (SKI) from the public key.
|
||||
- A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not
|
||||
provide one.
|
||||
- A value of C(always_create) always creates a SKI. If the CSR provides one, that one is
|
||||
ignored.
|
||||
- A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used.
|
||||
- This is only used by the C(ownca) provider.
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
type: str
|
||||
choices: [create_if_not_provided, always_create, never_create]
|
||||
default: create_if_not_provided
|
||||
|
||||
ownca_create_authority_key_identifier:
|
||||
description:
|
||||
- Create a Authority Key Identifier from the CA's certificate. If the CSR provided
|
||||
a authority key identifier, it is ignored.
|
||||
- The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier,
|
||||
if available. If it is not available, the CA certificate's public key will be used.
|
||||
- This is only used by the C(ownca) provider.
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
type: bool
|
||||
default: yes
|
||||
'''
|
||||
|
||||
BACKEND_SELFSIGNED_DOCUMENTATION = r'''
|
||||
notes:
|
||||
- For the C(selfsigned) provider, I(csr_path) and I(csr_content) are optional. If not provided, a
|
||||
certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created.
|
||||
|
||||
options:
|
||||
# NOTE: descriptions in options are overwritten, not appended. For that reason, the texts provided
|
||||
# here for csr_path and csr_content are not visible to the user. That's why this information is
|
||||
# added to the notes (see above).
|
||||
|
||||
# csr_path:
|
||||
# description:
|
||||
# - This is optional for the C(selfsigned) provider. If not provided, a certificate
|
||||
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
|
||||
# created.
|
||||
|
||||
# csr_content:
|
||||
# description:
|
||||
# - This is optional for the C(selfsigned) provider. If not provided, a certificate
|
||||
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
|
||||
# created.
|
||||
|
||||
selfsigned_version:
|
||||
description:
|
||||
- Version of the C(selfsigned) certificate.
|
||||
- Nowadays it should almost always be C(3).
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
type: int
|
||||
default: 3
|
||||
|
||||
selfsigned_digest:
|
||||
description:
|
||||
- Digest algorithm to be used when self-signing the certificate.
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
type: str
|
||||
default: sha256
|
||||
|
||||
selfsigned_not_before:
|
||||
description:
|
||||
- The point in time the certificate is valid from.
|
||||
- Time can be specified either as relative time or as absolute timestamp.
|
||||
- Time will always be interpreted as UTC.
|
||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- If this value is not specified, the certificate will start being valid from now.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
type: str
|
||||
default: +0s
|
||||
aliases: [ selfsigned_notBefore ]
|
||||
|
||||
selfsigned_not_after:
|
||||
description:
|
||||
- The point in time at which the certificate stops being valid.
|
||||
- Time can be specified either as relative time or as absolute timestamp.
|
||||
- Time will always be interpreted as UTC.
|
||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
|
||||
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||
type: str
|
||||
default: +3650d
|
||||
aliases: [ selfsigned_notAfter ]
|
||||
|
||||
selfsigned_create_subject_key_identifier:
|
||||
description:
|
||||
- Whether to create the Subject Key Identifier (SKI) from the public key.
|
||||
- A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not
|
||||
provide one.
|
||||
- A value of C(always_create) always creates a SKI. If the CSR provides one, that one is
|
||||
ignored.
|
||||
- A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used.
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
type: str
|
||||
choices: [create_if_not_provided, always_create, never_create]
|
||||
default: create_if_not_provided
|
||||
'''
|
||||
318
plugins/doc_fragments/module_csr.py
Normal file
318
plugins/doc_fragments/module_csr.py
Normal file
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyrigt: (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL certificate signing requests.
|
||||
- This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple
|
||||
extensions.
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
requirements:
|
||||
- Either cryptography >= 1.3
|
||||
- Or pyOpenSSL >= 0.15
|
||||
options:
|
||||
digest:
|
||||
description:
|
||||
- The digest used when signing the certificate signing request with the private key.
|
||||
type: str
|
||||
default: sha256
|
||||
privatekey_path:
|
||||
description:
|
||||
- The path to the private key to use when signing the certificate signing request.
|
||||
- Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
|
||||
type: path
|
||||
privatekey_content:
|
||||
description:
|
||||
- The content of the private key to use when signing the certificate signing request.
|
||||
- Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
|
||||
type: str
|
||||
privatekey_passphrase:
|
||||
description:
|
||||
- The passphrase for the private key.
|
||||
- This is required if the private key is password protected.
|
||||
type: str
|
||||
version:
|
||||
description:
|
||||
- The version of the certificate signing request.
|
||||
- "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1)
|
||||
is 1."
|
||||
- This option will no longer accept unsupported values from community.crypto 2.0.0 on.
|
||||
type: int
|
||||
default: 1
|
||||
subject:
|
||||
description:
|
||||
- Key/value pairs that will be present in the subject name field of the certificate signing request.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
type: dict
|
||||
country_name:
|
||||
description:
|
||||
- The countryName field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ C, countryName ]
|
||||
state_or_province_name:
|
||||
description:
|
||||
- The stateOrProvinceName field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ ST, stateOrProvinceName ]
|
||||
locality_name:
|
||||
description:
|
||||
- The localityName field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ L, localityName ]
|
||||
organization_name:
|
||||
description:
|
||||
- The organizationName field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ O, organizationName ]
|
||||
organizational_unit_name:
|
||||
description:
|
||||
- The organizationalUnitName field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ OU, organizationalUnitName ]
|
||||
common_name:
|
||||
description:
|
||||
- The commonName field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ CN, commonName ]
|
||||
email_address:
|
||||
description:
|
||||
- The emailAddress field of the certificate signing request subject.
|
||||
type: str
|
||||
aliases: [ E, emailAddress ]
|
||||
subject_alt_name:
|
||||
description:
|
||||
- Subject Alternative Name (SAN) extension to attach to the certificate signing request.
|
||||
- This can either be a 'comma separated string' or a YAML list.
|
||||
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
|
||||
C(otherName) and the ones specific to your CA).
|
||||
- Note that if no SAN is specified, but a common name, the common
|
||||
name will be added as a SAN except if C(useCommonNameForSAN) is
|
||||
set to I(false).
|
||||
- More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ subjectAltName ]
|
||||
subject_alt_name_critical:
|
||||
description:
|
||||
- Should the subjectAltName extension be considered as critical.
|
||||
type: bool
|
||||
default: false
|
||||
aliases: [ subjectAltName_critical ]
|
||||
use_common_name_for_san:
|
||||
description:
|
||||
- If set to C(yes), the module will fill the common name in for
|
||||
C(subject_alt_name) with C(DNS:) prefix if no SAN is specified.
|
||||
type: bool
|
||||
default: yes
|
||||
aliases: [ useCommonNameForSAN ]
|
||||
key_usage:
|
||||
description:
|
||||
- This defines the purpose (e.g. encipherment, signature, certificate signing)
|
||||
of the key contained in the certificate.
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ keyUsage ]
|
||||
key_usage_critical:
|
||||
description:
|
||||
- Should the keyUsage extension be considered as critical.
|
||||
type: bool
|
||||
default: false
|
||||
aliases: [ keyUsage_critical ]
|
||||
extended_key_usage:
|
||||
description:
|
||||
- Additional restrictions (e.g. client authentication, server authentication)
|
||||
on the allowed purposes for which the public key may be used.
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ extKeyUsage, extendedKeyUsage ]
|
||||
extended_key_usage_critical:
|
||||
description:
|
||||
- Should the extkeyUsage extension be considered as critical.
|
||||
type: bool
|
||||
default: false
|
||||
aliases: [ extKeyUsage_critical, extendedKeyUsage_critical ]
|
||||
basic_constraints:
|
||||
description:
|
||||
- Indicates basic constraints, such as if the certificate is a CA.
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ basicConstraints ]
|
||||
basic_constraints_critical:
|
||||
description:
|
||||
- Should the basicConstraints extension be considered as critical.
|
||||
type: bool
|
||||
default: false
|
||||
aliases: [ basicConstraints_critical ]
|
||||
ocsp_must_staple:
|
||||
description:
|
||||
- Indicates that the certificate should contain the OCSP Must Staple
|
||||
extension (U(https://tools.ietf.org/html/rfc7633)).
|
||||
type: bool
|
||||
default: false
|
||||
aliases: [ ocspMustStaple ]
|
||||
ocsp_must_staple_critical:
|
||||
description:
|
||||
- Should the OCSP Must Staple extension be considered as critical.
|
||||
- Note that according to the RFC, this extension should not be marked
|
||||
as critical, as old clients not knowing about OCSP Must Staple
|
||||
are required to reject such certificates
|
||||
(see U(https://tools.ietf.org/html/rfc7633#section-4)).
|
||||
type: bool
|
||||
default: false
|
||||
aliases: [ ocspMustStaple_critical ]
|
||||
name_constraints_permitted:
|
||||
description:
|
||||
- For CA certificates, this specifies a list of identifiers which describe
|
||||
subtrees of names that this CA is allowed to issue certificates for.
|
||||
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
|
||||
C(otherName) and the ones specific to your CA).
|
||||
type: list
|
||||
elements: str
|
||||
name_constraints_excluded:
|
||||
description:
|
||||
- For CA certificates, this specifies a list of identifiers which describe
|
||||
subtrees of names that this CA is B(not) allowed to issue certificates for.
|
||||
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
|
||||
C(otherName) and the ones specific to your CA).
|
||||
type: list
|
||||
elements: str
|
||||
name_constraints_critical:
|
||||
description:
|
||||
- Should the Name Constraints extension be considered as critical.
|
||||
type: bool
|
||||
default: false
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
create_subject_key_identifier:
|
||||
description:
|
||||
- Create the Subject Key Identifier from the public key.
|
||||
- "Please note that commercial CAs can ignore the value, respectively use a value of
|
||||
their own choice instead. Specifying this option is mostly useful for self-signed
|
||||
certificates or for own CAs."
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
type: bool
|
||||
default: no
|
||||
subject_key_identifier:
|
||||
description:
|
||||
- The subject key identifier as a hex string, where two bytes are separated by colons.
|
||||
- "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)"
|
||||
- "Please note that commercial CAs ignore this value, respectively use a value of their
|
||||
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||
or for own CAs."
|
||||
- Note that this option can only be used if I(create_subject_key_identifier) is C(no).
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
type: str
|
||||
authority_key_identifier:
|
||||
description:
|
||||
- The authority key identifier as a hex string, where two bytes are separated by colons.
|
||||
- "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)"
|
||||
- "Please note that commercial CAs ignore this value, respectively use a value of their
|
||||
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||
or for own CAs."
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
|
||||
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||
type: str
|
||||
authority_cert_issuer:
|
||||
description:
|
||||
- Names that will be present in the authority cert issuer field of the certificate signing request.
|
||||
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
|
||||
C(otherName) and the ones specific to your CA)
|
||||
- "Example: C(DNS:ca.example.org)"
|
||||
- If specified, I(authority_cert_serial_number) must also be specified.
|
||||
- "Please note that commercial CAs ignore this value, respectively use a value of their
|
||||
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||
or for own CAs."
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
|
||||
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||
type: list
|
||||
elements: str
|
||||
authority_cert_serial_number:
|
||||
description:
|
||||
- The authority cert serial number.
|
||||
- If specified, I(authority_cert_issuer) must also be specified.
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
- "Please note that commercial CAs ignore this value, respectively use a value of their
|
||||
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||
or for own CAs."
|
||||
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
|
||||
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||
type: int
|
||||
crl_distribution_points:
|
||||
description:
|
||||
- Allows to specify one or multiple CRL distribution points.
|
||||
- Only supported by the C(cryptography) backend.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
full_name:
|
||||
description:
|
||||
- Describes how the CRL can be retrieved.
|
||||
- Mutually exclusive with I(relative_name).
|
||||
- "Example: C(URI:https://ca.example.com/revocations.crl)."
|
||||
type: list
|
||||
elements: str
|
||||
relative_name:
|
||||
description:
|
||||
- Describes how the CRL can be retrieved relative to the CRL issuer.
|
||||
- Mutually exclusive with I(full_name).
|
||||
- "Example: C(/CN=example.com)."
|
||||
- Can only be used when cryptography >= 1.6 is installed.
|
||||
type: list
|
||||
elements: str
|
||||
crl_issuer:
|
||||
description:
|
||||
- Information about the issuer of the CRL.
|
||||
type: list
|
||||
elements: str
|
||||
reasons:
|
||||
description:
|
||||
- List of reasons that this distribution point can be used for when performing revocation checks.
|
||||
type: list
|
||||
elements: str
|
||||
choices:
|
||||
- key_compromise
|
||||
- ca_compromise
|
||||
- affiliation_changed
|
||||
- superseded
|
||||
- cessation_of_operation
|
||||
- certificate_hold
|
||||
- privilege_withdrawn
|
||||
- aa_compromise
|
||||
version_added: 1.4.0
|
||||
notes:
|
||||
- If the certificate signing request already exists it will be checked whether subjectAltName,
|
||||
keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether
|
||||
OCSP Must Staple is as requested, and if the request was signed by the given private key.
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.x509_certificate_pipe
|
||||
- module: community.crypto.openssl_dhparam
|
||||
- module: community.crypto.openssl_pkcs12
|
||||
- module: community.crypto.openssl_privatekey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
- module: community.crypto.openssl_publickey
|
||||
- module: community.crypto.openssl_csr_info
|
||||
'''
|
||||
163
plugins/doc_fragments/module_privatekey.py
Normal file
163
plugins/doc_fragments/module_privatekey.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
description:
|
||||
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29),
|
||||
L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
|
||||
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or
|
||||
L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys.
|
||||
- Keys are generated in PEM format.
|
||||
- "Please note that the module regenerates private keys if they don't match
|
||||
the module's options. In particular, if you provide another passphrase
|
||||
(or specify none), change the keysize, etc., the private key will be
|
||||
regenerated. If you are concerned that this could B(overwrite your private key),
|
||||
consider using the I(backup) option."
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
requirements:
|
||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Or pyOpenSSL
|
||||
options:
|
||||
size:
|
||||
description:
|
||||
- Size (in bits) of the TLS/SSL key to generate.
|
||||
type: int
|
||||
default: 4096
|
||||
type:
|
||||
description:
|
||||
- The algorithm used to generate the TLS/SSL private key.
|
||||
- Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend.
|
||||
C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require
|
||||
cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the
|
||||
I(curve) option.
|
||||
type: str
|
||||
default: RSA
|
||||
choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ]
|
||||
curve:
|
||||
description:
|
||||
- Note that not all curves are supported by all versions of C(cryptography).
|
||||
- For maximal interoperability, C(secp384r1) or C(secp256r1) should be used.
|
||||
- We use the curve names as defined in the
|
||||
L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
|
||||
- Please note that all curves except C(secp224r1), C(secp256k1), C(secp256r1), C(secp384r1) and C(secp521r1)
|
||||
are discouraged for new private keys.
|
||||
type: str
|
||||
choices:
|
||||
- secp224r1
|
||||
- secp256k1
|
||||
- secp256r1
|
||||
- secp384r1
|
||||
- secp521r1
|
||||
- secp192r1
|
||||
- brainpoolP256r1
|
||||
- brainpoolP384r1
|
||||
- brainpoolP512r1
|
||||
- sect163k1
|
||||
- sect163r2
|
||||
- sect233k1
|
||||
- sect233r1
|
||||
- sect283k1
|
||||
- sect283r1
|
||||
- sect409k1
|
||||
- sect409r1
|
||||
- sect571k1
|
||||
- sect571r1
|
||||
passphrase:
|
||||
description:
|
||||
- The passphrase for the private key.
|
||||
type: str
|
||||
cipher:
|
||||
description:
|
||||
- The cipher to encrypt the private key. (Valid values can be found by
|
||||
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
|
||||
depending on your OpenSSL version.)
|
||||
- When using the C(cryptography) backend, use C(auto).
|
||||
type: str
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
format:
|
||||
description:
|
||||
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
|
||||
is used for all keys which support it. Please note that not every key can be exported in any format.
|
||||
- The value C(auto) selects a format based on the key format. The value C(auto_ignore) does the same,
|
||||
but for existing private key files, it will not force a regenerate when its format is not the automatically
|
||||
selected one for generation.
|
||||
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default.
|
||||
To change this behavior, use the I(format_mismatch) option.
|
||||
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
|
||||
fail if a value different from C(auto_ignore) is used.
|
||||
type: str
|
||||
default: auto_ignore
|
||||
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
|
||||
format_mismatch:
|
||||
description:
|
||||
- Determines behavior of the module if the format of a private key does not match the expected format, but all
|
||||
other parameters are as expected.
|
||||
- If set to C(regenerate) (default), generates a new private key.
|
||||
- If set to C(convert), the key will be converted to the new format instead.
|
||||
- Only supported by the C(cryptography) backend.
|
||||
type: str
|
||||
default: regenerate
|
||||
choices: [ regenerate, convert ]
|
||||
regenerate:
|
||||
description:
|
||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
||||
The module will always generate a new key if the destination file does not exist.
|
||||
- By default, the key will be regenerated when it doesn't match the module's options,
|
||||
except when the key cannot be read or the passphrase does not match. Please note that
|
||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
||||
is specified.
|
||||
- If set to C(never), the module will fail if the key cannot be read or the passphrase
|
||||
isn't matching, and will never regenerate an existing key.
|
||||
- If set to C(fail), the module will fail if the key does not correspond to the module's
|
||||
options.
|
||||
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
|
||||
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
|
||||
the key is protected by an unknown passphrase, or when they key is not protected by a
|
||||
passphrase, but a passphrase is specified.
|
||||
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
|
||||
module's options. This is also the case if the key cannot be read (broken file), the key
|
||||
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
|
||||
but a passphrase is specified. Make sure you have a B(backup) when using this option!
|
||||
- If set to C(always), the module will always regenerate the key. This is equivalent to
|
||||
setting I(force) to C(yes).
|
||||
- Note that if I(format_mismatch) is set to C(convert) and everything matches except the
|
||||
format, the key will always be converted, except if I(regenerate) is set to C(always).
|
||||
type: str
|
||||
choices:
|
||||
- never
|
||||
- fail
|
||||
- partial_idempotence
|
||||
- full_idempotence
|
||||
- always
|
||||
default: full_idempotence
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.x509_certificate_pipe
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_csr_pipe
|
||||
- module: community.crypto.openssl_dhparam
|
||||
- module: community.crypto.openssl_pkcs12
|
||||
- module: community.crypto.openssl_publickey
|
||||
'''
|
||||
343
plugins/module_utils/_version.py
Normal file
343
plugins/module_utils/_version.py
Normal file
@@ -0,0 +1,343 @@
|
||||
# Vendored copy of distutils/version.py from CPython 3.9.5
|
||||
#
|
||||
# Implements multiple version numbering conventions for the
|
||||
# Python Module Distribution Utilities.
|
||||
#
|
||||
# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0)
|
||||
#
|
||||
|
||||
"""Provides classes to represent module version numbers (one class for
|
||||
each style of version numbering). There are currently two such classes
|
||||
implemented: StrictVersion and LooseVersion.
|
||||
|
||||
Every version number class implements the following interface:
|
||||
* the 'parse' method takes a string and parses it to some internal
|
||||
representation; if the string is an invalid version number,
|
||||
'parse' raises a ValueError exception
|
||||
* the class constructor takes an optional string argument which,
|
||||
if supplied, is passed to 'parse'
|
||||
* __str__ reconstructs the string that was passed to 'parse' (or
|
||||
an equivalent string -- ie. one that will generate an equivalent
|
||||
version number instance)
|
||||
* __repr__ generates Python code to recreate the version number instance
|
||||
* _cmp compares the current instance with either another instance
|
||||
of the same class or a string (which will be parsed to an instance
|
||||
of the same class, thus must follow the same rules)
|
||||
"""
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
try:
|
||||
RE_FLAGS = re.VERBOSE | re.ASCII
|
||||
except AttributeError:
|
||||
RE_FLAGS = re.VERBOSE
|
||||
|
||||
|
||||
class Version:
|
||||
"""Abstract base class for version numbering classes. Just provides
|
||||
constructor (__init__) and reproducer (__repr__), because those
|
||||
seem to be the same for all version numbering classes; and route
|
||||
rich comparisons to _cmp.
|
||||
"""
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s ('%s')" % (self.__class__.__name__, str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c == 0
|
||||
|
||||
def __lt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c < 0
|
||||
|
||||
def __le__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c <= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c > 0
|
||||
|
||||
def __ge__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c >= 0
|
||||
|
||||
|
||||
# Interface for version-number classes -- must be implemented
|
||||
# by the following classes (the concrete ones -- Version should
|
||||
# be treated as an abstract class).
|
||||
# __init__ (string) - create and take same action as 'parse'
|
||||
# (string parameter is optional)
|
||||
# parse (string) - convert a string representation to whatever
|
||||
# internal representation is appropriate for
|
||||
# this style of version numbering
|
||||
# __str__ (self) - convert back to a string; should be very similar
|
||||
# (if not identical to) the string supplied to parse
|
||||
# __repr__ (self) - generate Python code to recreate
|
||||
# the instance
|
||||
# _cmp (self, other) - compare two version numbers ('other' may
|
||||
# be an unparsed version string, or another
|
||||
# instance of your version class)
|
||||
|
||||
|
||||
class StrictVersion(Version):
|
||||
"""Version numbering for anal retentives and software idealists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of two or three
|
||||
dot-separated numeric components, with an optional "pre-release" tag
|
||||
on the end. The pre-release tag consists of the letter 'a' or 'b'
|
||||
followed by a number. If the numeric components of two version
|
||||
numbers are equal, then one with a pre-release tag will always
|
||||
be deemed earlier (lesser) than one without.
|
||||
|
||||
The following are valid version numbers (shown in the order that
|
||||
would be obtained by sorting according to the supplied cmp function):
|
||||
|
||||
0.4 0.4.0 (these two are equivalent)
|
||||
0.4.1
|
||||
0.5a1
|
||||
0.5b3
|
||||
0.5
|
||||
0.9.6
|
||||
1.0
|
||||
1.0.4a3
|
||||
1.0.4b1
|
||||
1.0.4
|
||||
|
||||
The following are examples of invalid version numbers:
|
||||
|
||||
1
|
||||
2.7.2.2
|
||||
1.3.a4
|
||||
1.3pl1
|
||||
1.3c4
|
||||
|
||||
The rationale for this version numbering system will be explained
|
||||
in the distutils documentation.
|
||||
"""
|
||||
|
||||
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
|
||||
RE_FLAGS)
|
||||
|
||||
def parse(self, vstring):
|
||||
match = self.version_re.match(vstring)
|
||||
if not match:
|
||||
raise ValueError("invalid version number '%s'" % vstring)
|
||||
|
||||
(major, minor, patch, prerelease, prerelease_num) = \
|
||||
match.group(1, 2, 4, 5, 6)
|
||||
|
||||
if patch:
|
||||
self.version = tuple(map(int, [major, minor, patch]))
|
||||
else:
|
||||
self.version = tuple(map(int, [major, minor])) + (0,)
|
||||
|
||||
if prerelease:
|
||||
self.prerelease = (prerelease[0], int(prerelease_num))
|
||||
else:
|
||||
self.prerelease = None
|
||||
|
||||
def __str__(self):
|
||||
if self.version[2] == 0:
|
||||
vstring = '.'.join(map(str, self.version[0:2]))
|
||||
else:
|
||||
vstring = '.'.join(map(str, self.version))
|
||||
|
||||
if self.prerelease:
|
||||
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
|
||||
|
||||
return vstring
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = StrictVersion(other)
|
||||
elif not isinstance(other, StrictVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version != other.version:
|
||||
# numeric versions don't match
|
||||
# prerelease stuff doesn't matter
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
# have to compare prerelease
|
||||
# case 1: neither has prerelease; they're equal
|
||||
# case 2: self has prerelease, other doesn't; other is greater
|
||||
# case 3: self doesn't have prerelease, other does: self is greater
|
||||
# case 4: both have prerelease: must compare them!
|
||||
|
||||
if (not self.prerelease and not other.prerelease):
|
||||
return 0
|
||||
elif (self.prerelease and not other.prerelease):
|
||||
return -1
|
||||
elif (not self.prerelease and other.prerelease):
|
||||
return 1
|
||||
elif (self.prerelease and other.prerelease):
|
||||
if self.prerelease == other.prerelease:
|
||||
return 0
|
||||
elif self.prerelease < other.prerelease:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
else:
|
||||
raise AssertionError("never get here")
|
||||
|
||||
# end class StrictVersion
|
||||
|
||||
# The rules according to Greg Stein:
|
||||
# 1) a version number has 1 or more numbers separated by a period or by
|
||||
# sequences of letters. If only periods, then these are compared
|
||||
# left-to-right to determine an ordering.
|
||||
# 2) sequences of letters are part of the tuple for comparison and are
|
||||
# compared lexicographically
|
||||
# 3) recognize the numeric components may have leading zeroes
|
||||
#
|
||||
# The LooseVersion class below implements these rules: a version number
|
||||
# string is split up into a tuple of integer and string components, and
|
||||
# comparison is a simple tuple comparison. This means that version
|
||||
# numbers behave in a predictable and obvious way, but a way that might
|
||||
# not necessarily be how people *want* version numbers to behave. There
|
||||
# wouldn't be a problem if people could stick to purely numeric version
|
||||
# numbers: just split on period and compare the numbers as tuples.
|
||||
# However, people insist on putting letters into their version numbers;
|
||||
# the most common purpose seems to be:
|
||||
# - indicating a "pre-release" version
|
||||
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
|
||||
# - indicating a post-release patch ('p', 'pl', 'patch')
|
||||
# but of course this can't cover all version number schemes, and there's
|
||||
# no way to know what a programmer means without asking him.
|
||||
#
|
||||
# The problem is what to do with letters (and other non-numeric
|
||||
# characters) in a version number. The current implementation does the
|
||||
# obvious and predictable thing: keep them as strings and compare
|
||||
# lexically within a tuple comparison. This has the desired effect if
|
||||
# an appended letter sequence implies something "post-release":
|
||||
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
|
||||
#
|
||||
# However, if letters in a version number imply a pre-release version,
|
||||
# the "obvious" thing isn't correct. Eg. you would expect that
|
||||
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
|
||||
# implemented here, this just isn't so.
|
||||
#
|
||||
# Two possible solutions come to mind. The first is to tie the
|
||||
# comparison algorithm to a particular set of semantic rules, as has
|
||||
# been done in the StrictVersion class above. This works great as long
|
||||
# as everyone can go along with bondage and discipline. Hopefully a
|
||||
# (large) subset of Python module programmers will agree that the
|
||||
# particular flavour of bondage and discipline provided by StrictVersion
|
||||
# provides enough benefit to be worth using, and will submit their
|
||||
# version numbering scheme to its domination. The free-thinking
|
||||
# anarchists in the lot will never give in, though, and something needs
|
||||
# to be done to accommodate them.
|
||||
#
|
||||
# Perhaps a "moderately strict" version class could be implemented that
|
||||
# lets almost anything slide (syntactically), and makes some heuristic
|
||||
# assumptions about non-digits in version number strings. This could
|
||||
# sink into special-case-hell, though; if I was as talented and
|
||||
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
|
||||
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
|
||||
# just as happy dealing with things like "2g6" and "1.13++". I don't
|
||||
# think I'm smart enough to do it right though.
|
||||
#
|
||||
# In any case, I've coded the test suite for this module (see
|
||||
# ../test/test_version.py) specifically to fail on things like comparing
|
||||
# "1.2a2" and "1.2". That's not because the *code* is doing anything
|
||||
# wrong, it's because the simple, obvious design doesn't match my
|
||||
# complicated, hairy expectations for real-world version numbers. It
|
||||
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
|
||||
# the Right Thing" (ie. the code matches the conception). But I'd rather
|
||||
# have a conception that matches common notions about version numbers.
|
||||
|
||||
|
||||
class LooseVersion(Version):
|
||||
"""Version numbering for anarchists and software realists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of a series of numbers,
|
||||
separated by either periods or strings of letters. When comparing
|
||||
version numbers, the numeric components will be compared
|
||||
numerically, and the alphabetic components lexically. The following
|
||||
are all valid version numbers, in no particular order:
|
||||
|
||||
1.5.1
|
||||
1.5.2b2
|
||||
161
|
||||
3.10a
|
||||
8.02
|
||||
3.4j
|
||||
1996.07.12
|
||||
3.2.pl0
|
||||
3.1.1.6
|
||||
2g6
|
||||
11g
|
||||
0.960923
|
||||
2.2beta29
|
||||
1.13++
|
||||
5.5.kw
|
||||
2.0b1pl0
|
||||
|
||||
In fact, there is no such thing as an invalid version number under
|
||||
this scheme; the rules for comparison are simple and predictable,
|
||||
but may not always give the results you want (for some definition
|
||||
of "want").
|
||||
"""
|
||||
|
||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def parse(self, vstring):
|
||||
# I've given up on thinking I can reconstruct the version string
|
||||
# from the parsed tuple -- so I just store the string here for
|
||||
# use by __str__
|
||||
self.vstring = vstring
|
||||
components = [x for x in self.component_re.split(vstring) if x and x != '.']
|
||||
for i, obj in enumerate(components):
|
||||
try:
|
||||
components[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.version = components
|
||||
|
||||
def __str__(self):
|
||||
return self.vstring
|
||||
|
||||
def __repr__(self):
|
||||
return "LooseVersion ('%s')" % str(self)
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = LooseVersion(other)
|
||||
elif not isinstance(other, LooseVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version == other.version:
|
||||
return 0
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
if self.version > other.version:
|
||||
return 1
|
||||
|
||||
# end class LooseVersion
|
||||
File diff suppressed because it is too large
Load Diff
90
plugins/module_utils/acme/__init__.py
Normal file
90
plugins/module_utils/acme/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import copy
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six.moves.urllib.parse import unquote
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
get_default_argspec,
|
||||
ACMEDirectory,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
|
||||
CryptographyBackend,
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||
OpenSSLCLIBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme._compatibility import (
|
||||
handle_standard_module_arguments,
|
||||
set_crypto_backend,
|
||||
HAS_CURRENT_CRYPTOGRAPHY,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme._compatibility import ACMELegacyAccount as ACMEAccount
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
|
||||
read_file,
|
||||
write_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
pem_to_der,
|
||||
process_links,
|
||||
)
|
||||
|
||||
|
||||
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename, csr_content=None):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
return OpenSSLCLIBackend(module, openssl_binary=openssl_binary).get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
|
||||
|
||||
|
||||
def cryptography_get_csr_identifiers(module, csr_filename, csr_content=None):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
return CryptographyBackend(module).get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
|
||||
|
||||
|
||||
def cryptography_get_cert_days(module, cert_file, now=None):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
return CryptographyBackend(module).get_cert_days(cert_filename=cert_file, now=now)
|
||||
267
plugins/module_utils/acme/_compatibility.py
Normal file
267
plugins/module_utils/acme/_compatibility.py
Normal file
@@ -0,0 +1,267 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import locale
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import HAS_CURRENT_CRYPTOGRAPHY as _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
|
||||
CryptographyBackend,
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||
OpenSSLCLIBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
ACMEClient,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
|
||||
ACMEAccount,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
|
||||
create_key_authorization,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
|
||||
HAS_CURRENT_CRYPTOGRAPHY = _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY
|
||||
|
||||
|
||||
def set_crypto_backend(module):
|
||||
'''
|
||||
Sets which crypto backend to use (default: auto detection).
|
||||
|
||||
Does not care whether a new enough cryptoraphy is available or not. Must
|
||||
be called before any real stuff is done which might evaluate
|
||||
``HAS_CURRENT_CRYPTOGRAPHY``.
|
||||
'''
|
||||
global HAS_CURRENT_CRYPTOGRAPHY
|
||||
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
# Choose backend
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
pass
|
||||
elif backend == 'openssl':
|
||||
HAS_CURRENT_CRYPTOGRAPHY = False
|
||||
elif backend == 'cryptography':
|
||||
if not _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY:
|
||||
module.fail_json(msg=missing_required_lib('cryptography'))
|
||||
HAS_CURRENT_CRYPTOGRAPHY = True
|
||||
else:
|
||||
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
|
||||
# Inform about choices
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
|
||||
return 'cryptography'
|
||||
else:
|
||||
module.debug('Using OpenSSL binary backend')
|
||||
return 'openssl'
|
||||
|
||||
|
||||
def handle_standard_module_arguments(module, needs_acme_v2=False):
|
||||
'''
|
||||
Do standard module setup, argument handling and warning emitting.
|
||||
'''
|
||||
backend = set_crypto_backend(module)
|
||||
|
||||
if not module.params['validate_certs']:
|
||||
module.warn(
|
||||
'Disabling certificate validation for communications with ACME endpoint. '
|
||||
'This should only be done for testing against a local ACME server for '
|
||||
'development purposes, but *never* for production purposes.'
|
||||
)
|
||||
|
||||
if module.params['acme_version'] is None:
|
||||
module.params['acme_version'] = 1
|
||||
module.deprecate("The option 'acme_version' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if module.params['acme_directory'] is None:
|
||||
module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory'
|
||||
module.deprecate("The option 'acme_directory' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if needs_acme_v2 and module.params['acme_version'] < 2:
|
||||
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
|
||||
|
||||
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
|
||||
return backend
|
||||
|
||||
|
||||
def get_compatibility_backend(module):
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return CryptographyBackend(module)
|
||||
else:
|
||||
return OpenSSLCLIBackend(module)
|
||||
|
||||
|
||||
class ACMELegacyAccount(object):
|
||||
'''
|
||||
ACME account object. Handles the authorized communication with the
|
||||
ACME server. Provides access to account bound information like
|
||||
the currently active authorizations and valid certificates
|
||||
'''
|
||||
|
||||
def __init__(self, module):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
backend = get_compatibility_backend(module)
|
||||
self.client = ACMEClient(module, backend)
|
||||
self.account = ACMEAccount(self.client)
|
||||
self.key = self.client.account_key_file
|
||||
self.key_content = self.client.account_key_content
|
||||
self.uri = self.client.account_uri
|
||||
self.key_data = self.client.account_key_data
|
||||
self.jwk = self.client.account_jwk
|
||||
self.jws_header = self.client.account_jws_header
|
||||
self.directory = self.client.directory
|
||||
|
||||
def get_keyauthorization(self, token):
|
||||
'''
|
||||
Returns the key authorization for the given token
|
||||
https://tools.ietf.org/html/rfc8555#section-8.1
|
||||
'''
|
||||
return create_key_authorization(self.client, token)
|
||||
|
||||
def parse_key(self, key_file=None, key_content=None):
|
||||
'''
|
||||
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
|
||||
(error, key_data).
|
||||
'''
|
||||
try:
|
||||
return None, self.client.parse_key(key_file=key_file, key_content=key_content)
|
||||
except KeyParsingError as e:
|
||||
return e.msg, {}
|
||||
|
||||
def sign_request(self, protected, payload, key_data, encode_payload=True):
|
||||
return self.client.sign_request(protected, payload, key_data, encode_payload=encode_payload)
|
||||
|
||||
def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, encode_payload=True):
|
||||
'''
|
||||
Sends a JWS signed HTTP POST request to the ACME server and returns
|
||||
the response as dictionary
|
||||
https://tools.ietf.org/html/rfc8555#section-6.2
|
||||
|
||||
If payload is None, a POST-as-GET is performed.
|
||||
(https://tools.ietf.org/html/rfc8555#section-6.3)
|
||||
'''
|
||||
return self.client.send_signed_request(
|
||||
url,
|
||||
payload,
|
||||
key_data=key_data,
|
||||
jws_header=jws_header,
|
||||
parse_json_result=parse_json_result,
|
||||
encode_payload=encode_payload,
|
||||
fail_on_error=False,
|
||||
)
|
||||
|
||||
def get_request(self, uri, parse_json_result=True, headers=None, get_only=False, fail_on_error=True):
|
||||
'''
|
||||
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
|
||||
to GET if server replies with a status code of 405.
|
||||
'''
|
||||
return self.client.get_request(
|
||||
uri,
|
||||
parse_json_result=parse_json_result,
|
||||
headers=headers,
|
||||
get_only=get_only,
|
||||
fail_on_error=fail_on_error,
|
||||
)
|
||||
|
||||
def set_account_uri(self, uri):
|
||||
'''
|
||||
Set account URI. For ACME v2, it needs to be used to sending signed
|
||||
requests.
|
||||
'''
|
||||
self.client.set_account_uri(uri)
|
||||
self.uri = self.client.account_uri
|
||||
|
||||
def get_account_data(self):
|
||||
'''
|
||||
Retrieve account information. Can only be called when the account
|
||||
URI is already known (such as after calling setup_account).
|
||||
Return None if the account was deactivated, or a dict otherwise.
|
||||
'''
|
||||
return self.account.get_account_data()
|
||||
|
||||
def setup_account(self, contact=None, agreement=None, terms_agreed=False,
|
||||
allow_creation=True, remove_account_uri_if_not_exists=False,
|
||||
external_account_binding=None):
|
||||
'''
|
||||
Detect or create an account on the ACME server. For ACME v1,
|
||||
as the only way (without knowing an account URI) to test if an
|
||||
account exists is to try and create one with the provided account
|
||||
key, this method will always result in an account being present
|
||||
(except on error situations). For ACME v2, a new account will
|
||||
only be created if ``allow_creation`` is set to True.
|
||||
|
||||
For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
|
||||
account might be created if it does not yet exist.
|
||||
|
||||
Return a pair ``(created, account_data)``. Here, ``created`` will
|
||||
be ``True`` in case the account was created or would be created
|
||||
(check mode). ``account_data`` will be the current account data,
|
||||
or ``None`` if the account does not exist.
|
||||
|
||||
The account URI will be stored in ``self.uri``; if it is ``None``,
|
||||
the account does not exist.
|
||||
|
||||
If specified, ``external_account_binding`` should be a dictionary
|
||||
with keys ``kid``, ``alg`` and ``key``
|
||||
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3
|
||||
'''
|
||||
result = self.account.setup_account(
|
||||
contact=contact,
|
||||
agreement=agreement,
|
||||
terms_agreed=terms_agreed,
|
||||
allow_creation=allow_creation,
|
||||
remove_account_uri_if_not_exists=remove_account_uri_if_not_exists,
|
||||
external_account_binding=external_account_binding,
|
||||
)
|
||||
self.uri = self.client.account_uri
|
||||
return result
|
||||
|
||||
def update_account(self, account_data, contact=None):
|
||||
'''
|
||||
Update an account on the ACME server. Check mode is fully respected.
|
||||
|
||||
The current account data must be provided as ``account_data``.
|
||||
|
||||
Return a pair ``(updated, account_data)``, where ``updated`` is
|
||||
``True`` in case something changed (contact info updated) or
|
||||
would be changed (check mode), and ``account_data`` the updated
|
||||
account data.
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3.2
|
||||
'''
|
||||
return self.account.update_account(account_data, contact=contact)
|
||||
251
plugins/module_utils/acme/account.py
Normal file
251
plugins/module_utils/acme/account.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
ACMEProtocolException,
|
||||
ModuleFailException,
|
||||
)
|
||||
|
||||
|
||||
class ACMEAccount(object):
|
||||
'''
|
||||
ACME account object. Allows to create new accounts, check for existence of accounts,
|
||||
retrieve account data.
|
||||
'''
|
||||
|
||||
def __init__(self, client):
|
||||
# Set to true to enable logging of all signed requests
|
||||
self._debug = False
|
||||
|
||||
self.client = client
|
||||
|
||||
def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True,
|
||||
external_account_binding=None):
|
||||
'''
|
||||
Registers a new ACME account. Returns a pair ``(created, data)``.
|
||||
Here, ``created`` is ``True`` if the account was created and
|
||||
``False`` if it already existed (e.g. it was not newly created),
|
||||
or does not exist. In case the account was created or exists,
|
||||
``data`` contains the account data; otherwise, it is ``None``.
|
||||
|
||||
If specified, ``external_account_binding`` should be a dictionary
|
||||
with keys ``kid``, ``alg`` and ``key``
|
||||
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3
|
||||
'''
|
||||
contact = contact or []
|
||||
|
||||
if self.client.version == 1:
|
||||
new_reg = {
|
||||
'resource': 'new-reg',
|
||||
'contact': contact
|
||||
}
|
||||
if agreement:
|
||||
new_reg['agreement'] = agreement
|
||||
else:
|
||||
new_reg['agreement'] = self.client.directory['meta']['terms-of-service']
|
||||
if external_account_binding is not None:
|
||||
raise ModuleFailException('External account binding is not supported for ACME v1')
|
||||
url = self.client.directory['new-reg']
|
||||
else:
|
||||
if (external_account_binding is not None or self.client.directory['meta'].get('externalAccountRequired')) and allow_creation:
|
||||
# Some ACME servers such as ZeroSSL do not like it when you try to register an existing account
|
||||
# and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
|
||||
# to see whether the account already exists.
|
||||
|
||||
# Note that we pass contact here: ZeroSSL does not accept regisration calls without contacts, even
|
||||
# if onlyReturnExisting is set to true.
|
||||
created, data = self._new_reg(contact=contact, allow_creation=False)
|
||||
if data:
|
||||
# An account already exists! Return data
|
||||
return created, data
|
||||
# An account does not yet exist. Try to create one next.
|
||||
|
||||
new_reg = {
|
||||
'contact': contact
|
||||
}
|
||||
if not allow_creation:
|
||||
# https://tools.ietf.org/html/rfc8555#section-7.3.1
|
||||
new_reg['onlyReturnExisting'] = True
|
||||
if terms_agreed:
|
||||
new_reg['termsOfServiceAgreed'] = True
|
||||
url = self.client.directory['newAccount']
|
||||
if external_account_binding is not None:
|
||||
new_reg['externalAccountBinding'] = self.client.sign_request(
|
||||
{
|
||||
'alg': external_account_binding['alg'],
|
||||
'kid': external_account_binding['kid'],
|
||||
'url': url,
|
||||
},
|
||||
self.client.account_jwk,
|
||||
self.client.backend.create_mac_key(external_account_binding['alg'], external_account_binding['key'])
|
||||
)
|
||||
elif self.client.directory['meta'].get('externalAccountRequired') and allow_creation:
|
||||
raise ModuleFailException(
|
||||
'To create an account, an external account binding must be specified. '
|
||||
'Use the acme_account module with the external_account_binding option.'
|
||||
)
|
||||
|
||||
result, info = self.client.send_signed_request(url, new_reg, fail_on_error=False)
|
||||
|
||||
if info['status'] in ([200, 201] if self.client.version == 1 else [201]):
|
||||
# Account did not exist
|
||||
if 'location' in info:
|
||||
self.client.set_account_uri(info['location'])
|
||||
return True, result
|
||||
elif info['status'] == (409 if self.client.version == 1 else 200):
|
||||
# Account did exist
|
||||
if result.get('status') == 'deactivated':
|
||||
# A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and
|
||||
# Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should
|
||||
# not return a valid account object according to
|
||||
# https://tools.ietf.org/html/rfc8555#section-7.3.6:
|
||||
# "Once an account is deactivated, the server MUST NOT accept further
|
||||
# requests authorized by that account's key."
|
||||
if not allow_creation:
|
||||
return False, None
|
||||
else:
|
||||
raise ModuleFailException("Account is deactivated")
|
||||
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:
|
||||
# Account does not exist (and we didn't try to create it)
|
||||
return False, None
|
||||
elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''):
|
||||
# Account has been deactivated; currently works for Pebble; hasn't been
|
||||
# implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971),
|
||||
# might need adjustment in error detection.
|
||||
if not allow_creation:
|
||||
return False, None
|
||||
else:
|
||||
raise ModuleFailException("Account is deactivated")
|
||||
else:
|
||||
raise ACMEProtocolException(
|
||||
self.client.module, msg='Registering ACME account failed', info=info, content_json=result)
|
||||
|
||||
def get_account_data(self):
|
||||
'''
|
||||
Retrieve account information. Can only be called when the account
|
||||
URI is already known (such as after calling setup_account).
|
||||
Return None if the account was deactivated, or a dict otherwise.
|
||||
'''
|
||||
if self.client.account_uri is None:
|
||||
raise ModuleFailException("Account URI unknown")
|
||||
if self.client.version == 1:
|
||||
data = {}
|
||||
data['resource'] = 'reg'
|
||||
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
|
||||
else:
|
||||
# try POST-as-GET first (draft-15 or newer)
|
||||
data = None
|
||||
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
|
||||
# check whether that failed with a malformed request error
|
||||
if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed':
|
||||
# retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
|
||||
data = {}
|
||||
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
|
||||
if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
|
||||
# Returned when account is deactivated
|
||||
return None
|
||||
if info['status'] in (400, 404) and result.get('type') == 'urn:ietf:params:acme:error:accountDoesNotExist':
|
||||
# Returned when account does not exist
|
||||
return None
|
||||
if info['status'] < 200 or info['status'] >= 300:
|
||||
raise ACMEProtocolException(
|
||||
self.client.module, msg='Error retrieving account data', info=info, content_json=result)
|
||||
return result
|
||||
|
||||
def setup_account(self, contact=None, agreement=None, terms_agreed=False,
|
||||
allow_creation=True, remove_account_uri_if_not_exists=False,
|
||||
external_account_binding=None):
|
||||
'''
|
||||
Detect or create an account on the ACME server. For ACME v1,
|
||||
as the only way (without knowing an account URI) to test if an
|
||||
account exists is to try and create one with the provided account
|
||||
key, this method will always result in an account being present
|
||||
(except on error situations). For ACME v2, a new account will
|
||||
only be created if ``allow_creation`` is set to True.
|
||||
|
||||
For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
|
||||
account might be created if it does not yet exist.
|
||||
|
||||
Return a pair ``(created, account_data)``. Here, ``created`` will
|
||||
be ``True`` in case the account was created or would be created
|
||||
(check mode). ``account_data`` will be the current account data,
|
||||
or ``None`` if the account does not exist.
|
||||
|
||||
The account URI will be stored in ``client.account_uri``; if it is ``None``,
|
||||
the account does not exist.
|
||||
|
||||
If specified, ``external_account_binding`` should be a dictionary
|
||||
with keys ``kid``, ``alg`` and ``key``
|
||||
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3
|
||||
'''
|
||||
|
||||
if self.client.account_uri is not None:
|
||||
created = False
|
||||
# Verify that the account key belongs to the URI.
|
||||
# (If update_contact is True, this will be done below.)
|
||||
account_data = self.get_account_data()
|
||||
if account_data is None:
|
||||
if remove_account_uri_if_not_exists and not allow_creation:
|
||||
self.client.account_uri = None
|
||||
else:
|
||||
raise ModuleFailException("Account is deactivated or does not exist!")
|
||||
else:
|
||||
created, account_data = self._new_reg(
|
||||
contact,
|
||||
agreement=agreement,
|
||||
terms_agreed=terms_agreed,
|
||||
allow_creation=allow_creation and not self.client.module.check_mode,
|
||||
external_account_binding=external_account_binding,
|
||||
)
|
||||
if self.client.module.check_mode and self.client.account_uri is None and allow_creation:
|
||||
created = True
|
||||
account_data = {
|
||||
'contact': contact or []
|
||||
}
|
||||
return created, account_data
|
||||
|
||||
def update_account(self, account_data, contact=None):
|
||||
'''
|
||||
Update an account on the ACME server. Check mode is fully respected.
|
||||
|
||||
The current account data must be provided as ``account_data``.
|
||||
|
||||
Return a pair ``(updated, account_data)``, where ``updated`` is
|
||||
``True`` in case something changed (contact info updated) or
|
||||
would be changed (check mode), and ``account_data`` the updated
|
||||
account data.
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3.2
|
||||
'''
|
||||
# Create request
|
||||
update_request = {}
|
||||
if contact is not None and account_data.get('contact', []) != contact:
|
||||
update_request['contact'] = list(contact)
|
||||
|
||||
# No change?
|
||||
if not update_request:
|
||||
return False, dict(account_data)
|
||||
|
||||
# Apply change
|
||||
if self.client.module.check_mode:
|
||||
account_data = dict(account_data)
|
||||
account_data.update(update_request)
|
||||
else:
|
||||
if self.client.version == 1:
|
||||
update_request['resource'] = 'reg'
|
||||
account_data, dummy = self.client.send_signed_request(self.client.account_uri, update_request)
|
||||
return True, account_data
|
||||
383
plugins/module_utils/acme/acme.py
Normal file
383
plugins/module_utils/acme/acme.py
Normal file
@@ -0,0 +1,383 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import locale
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six import PY3
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||
OpenSSLCLIBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
|
||||
CryptographyBackend,
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
HAS_CURRENT_CRYPTOGRAPHY,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
ACMEProtocolException,
|
||||
NetworkException,
|
||||
ModuleFailException,
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
)
|
||||
|
||||
|
||||
def _assert_fetch_url_success(module, response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True):
|
||||
if info['status'] < 0:
|
||||
raise NetworkException(msg="Failure downloading %s, %s" % (info['url'], info['msg']))
|
||||
|
||||
if (300 <= info['status'] < 400 and not allow_redirect) or \
|
||||
(400 <= info['status'] < 500 and not allow_client_error) or \
|
||||
(info['status'] >= 500 and not allow_server_error):
|
||||
raise ACMEProtocolException(module, info=info, response=response)
|
||||
|
||||
|
||||
def _is_failed(info, expected_status_codes=None):
|
||||
if info['status'] < 200 or info['status'] >= 400:
|
||||
return True
|
||||
if expected_status_codes is not None and info['status'] not in expected_status_codes:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ACMEDirectory(object):
|
||||
'''
|
||||
The ACME server directory. Gives access to the available resources,
|
||||
and allows to obtain a Replay-Nonce. The acme_directory URL
|
||||
needs to support unauthenticated GET requests; ACME endpoints
|
||||
requiring authentication are not supported.
|
||||
https://tools.ietf.org/html/rfc8555#section-7.1.1
|
||||
'''
|
||||
|
||||
def __init__(self, module, account):
|
||||
self.module = module
|
||||
self.directory_root = module.params['acme_directory']
|
||||
self.version = module.params['acme_version']
|
||||
|
||||
self.directory, dummy = account.get_request(self.directory_root, get_only=True)
|
||||
|
||||
# Check whether self.version matches what we expect
|
||||
if self.version == 1:
|
||||
for key in ('new-reg', 'new-authz', 'new-cert'):
|
||||
if key not in self.directory:
|
||||
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v1")
|
||||
if self.version == 2:
|
||||
for key in ('newNonce', 'newAccount', 'newOrder'):
|
||||
if key not in self.directory:
|
||||
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2")
|
||||
# Make sure that 'meta' is always available
|
||||
if 'meta' not in self.directory:
|
||||
self.directory['meta'] = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.directory[key]
|
||||
|
||||
def get_nonce(self, resource=None):
|
||||
url = self.directory_root if self.version == 1 else self.directory['newNonce']
|
||||
if resource is not None:
|
||||
url = resource
|
||||
dummy, info = fetch_url(self.module, url, method='HEAD')
|
||||
if info['status'] not in (200, 204):
|
||||
raise NetworkException("Failed to get replay-nonce, got status {0}".format(info['status']))
|
||||
return info['replay-nonce']
|
||||
|
||||
|
||||
class ACMEClient(object):
|
||||
'''
|
||||
ACME client object. Handles the authorized communication with the
|
||||
ACME server.
|
||||
'''
|
||||
|
||||
def __init__(self, module, backend):
|
||||
# Set to true to enable logging of all signed requests
|
||||
self._debug = False
|
||||
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.version = module.params['acme_version']
|
||||
# account_key path and content are mutually exclusive
|
||||
self.account_key_file = module.params['account_key_src']
|
||||
self.account_key_content = module.params['account_key_content']
|
||||
self.account_key_passphrase = module.params['account_key_passphrase']
|
||||
|
||||
# Grab account URI from module parameters.
|
||||
# Make sure empty string is treated as None.
|
||||
self.account_uri = module.params.get('account_uri') or None
|
||||
|
||||
self.account_key_data = None
|
||||
self.account_jwk = None
|
||||
self.account_jws_header = None
|
||||
if self.account_key_file is not None or self.account_key_content is not None:
|
||||
try:
|
||||
self.account_key_data = self.parse_key(
|
||||
key_file=self.account_key_file,
|
||||
key_content=self.account_key_content,
|
||||
passphrase=self.account_key_passphrase)
|
||||
except KeyParsingError as e:
|
||||
raise ModuleFailException("Error while parsing account key: {msg}".format(msg=e.msg))
|
||||
self.account_jwk = self.account_key_data['jwk']
|
||||
self.account_jws_header = {
|
||||
"alg": self.account_key_data['alg'],
|
||||
"jwk": self.account_jwk,
|
||||
}
|
||||
if self.account_uri:
|
||||
# Make sure self.account_jws_header is updated
|
||||
self.set_account_uri(self.account_uri)
|
||||
|
||||
self.directory = ACMEDirectory(module, self)
|
||||
|
||||
def set_account_uri(self, uri):
|
||||
'''
|
||||
Set account URI. For ACME v2, it needs to be used to sending signed
|
||||
requests.
|
||||
'''
|
||||
self.account_uri = uri
|
||||
if self.version != 1:
|
||||
self.account_jws_header.pop('jwk')
|
||||
self.account_jws_header['kid'] = self.account_uri
|
||||
|
||||
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.
|
||||
In case of an error, raises KeyParsingError.
|
||||
'''
|
||||
if key_file is None and key_content is None:
|
||||
raise AssertionError('One of key_file and key_content must be specified!')
|
||||
return self.backend.parse_key(key_file, key_content, passphrase=passphrase)
|
||||
|
||||
def sign_request(self, protected, payload, key_data, encode_payload=True):
|
||||
'''
|
||||
Signs an ACME request.
|
||||
'''
|
||||
try:
|
||||
if payload is None:
|
||||
# POST-as-GET
|
||||
payload64 = ''
|
||||
else:
|
||||
# POST
|
||||
if encode_payload:
|
||||
payload = self.module.jsonify(payload).encode('utf8')
|
||||
payload64 = nopad_b64(to_bytes(payload))
|
||||
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
|
||||
except Exception as e:
|
||||
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
|
||||
|
||||
return self.backend.sign(payload64, protected64, key_data)
|
||||
|
||||
def _log(self, msg, data=None):
|
||||
'''
|
||||
Write arguments to acme.log when logging is enabled.
|
||||
'''
|
||||
if self._debug:
|
||||
with open('acme.log', 'ab') as f:
|
||||
f.write('[{0}] {1}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%s'), msg).encode('utf-8'))
|
||||
if data is not None:
|
||||
f.write('{0}\n\n'.format(json.dumps(data, indent=2, sort_keys=True)).encode('utf-8'))
|
||||
|
||||
def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True,
|
||||
encode_payload=True, fail_on_error=True, error_msg=None, expected_status_codes=None):
|
||||
'''
|
||||
Sends a JWS signed HTTP POST request to the ACME server and returns
|
||||
the response as dictionary (if parse_json_result is True) or in raw form
|
||||
(if parse_json_result is False).
|
||||
https://tools.ietf.org/html/rfc8555#section-6.2
|
||||
|
||||
If payload is None, a POST-as-GET is performed.
|
||||
(https://tools.ietf.org/html/rfc8555#section-6.3)
|
||||
'''
|
||||
key_data = key_data or self.account_key_data
|
||||
jws_header = jws_header or self.account_jws_header
|
||||
failed_tries = 0
|
||||
while True:
|
||||
protected = copy.deepcopy(jws_header)
|
||||
protected["nonce"] = self.directory.get_nonce()
|
||||
if self.version != 1:
|
||||
protected["url"] = url
|
||||
|
||||
self._log('URL', url)
|
||||
self._log('protected', protected)
|
||||
self._log('payload', payload)
|
||||
data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload)
|
||||
if self.version == 1:
|
||||
data["header"] = jws_header.copy()
|
||||
for k, v in protected.items():
|
||||
dummy = data["header"].pop(k, None)
|
||||
self._log('signed request', data)
|
||||
data = self.module.jsonify(data)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/jose+json',
|
||||
}
|
||||
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST')
|
||||
_assert_fetch_url_success(self.module, resp, info)
|
||||
result = {}
|
||||
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
# In Python 3, read() simply returns ''
|
||||
if PY3 and resp.closed:
|
||||
raise TypeError
|
||||
content = resp.read()
|
||||
except (AttributeError, TypeError):
|
||||
content = info.pop('body', None)
|
||||
|
||||
if content or not parse_json_result:
|
||||
if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600:
|
||||
try:
|
||||
decoded_result = self.module.from_json(content.decode('utf8'))
|
||||
self._log('parsed result', decoded_result)
|
||||
# In case of badNonce error, try again (up to 5 times)
|
||||
# (https://tools.ietf.org/html/rfc8555#section-6.7)
|
||||
if all((
|
||||
400 <= info['status'] < 600,
|
||||
decoded_result.get('type') == 'urn:ietf:params:acme:error:badNonce',
|
||||
failed_tries <= 5,
|
||||
)):
|
||||
failed_tries += 1
|
||||
continue
|
||||
if parse_json_result:
|
||||
result = decoded_result
|
||||
else:
|
||||
result = content
|
||||
except ValueError:
|
||||
raise NetworkException("Failed to parse the ACME response: {0} {1}".format(url, content))
|
||||
else:
|
||||
result = content
|
||||
|
||||
if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes):
|
||||
raise ACMEProtocolException(
|
||||
self.module, msg=error_msg, info=info, content=content, content_json=result if parse_json_result else None)
|
||||
return result, info
|
||||
|
||||
def get_request(self, uri, parse_json_result=True, headers=None, get_only=False,
|
||||
fail_on_error=True, error_msg=None, expected_status_codes=None):
|
||||
'''
|
||||
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
|
||||
to GET if server replies with a status code of 405.
|
||||
'''
|
||||
if not get_only and self.version != 1:
|
||||
# Try POST-as-GET
|
||||
content, info = self.send_signed_request(uri, None, parse_json_result=False, fail_on_error=False)
|
||||
if info['status'] == 405:
|
||||
# Instead, do unauthenticated GET
|
||||
get_only = True
|
||||
else:
|
||||
# Do unauthenticated GET
|
||||
get_only = True
|
||||
|
||||
if get_only:
|
||||
# Perform unauthenticated GET
|
||||
resp, info = fetch_url(self.module, uri, method='GET', headers=headers)
|
||||
|
||||
_assert_fetch_url_success(self.module, resp, info)
|
||||
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
# In Python 3, read() simply returns ''
|
||||
if PY3 and resp.closed:
|
||||
raise TypeError
|
||||
content = resp.read()
|
||||
except (AttributeError, TypeError):
|
||||
content = info.pop('body', None)
|
||||
|
||||
# Process result
|
||||
parsed_json_result = False
|
||||
if parse_json_result:
|
||||
result = {}
|
||||
if content:
|
||||
if info['content-type'].startswith('application/json'):
|
||||
try:
|
||||
result = self.module.from_json(content.decode('utf8'))
|
||||
parsed_json_result = True
|
||||
except ValueError:
|
||||
raise NetworkException("Failed to parse the ACME response: {0} {1}".format(uri, content))
|
||||
else:
|
||||
result = content
|
||||
else:
|
||||
result = content
|
||||
|
||||
if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes):
|
||||
raise ACMEProtocolException(
|
||||
self.module, msg=error_msg, info=info, content=content, content_json=result if parsed_json_result else None)
|
||||
return result, info
|
||||
|
||||
|
||||
def get_default_argspec():
|
||||
'''
|
||||
Provides default argument spec for the options documented in the acme doc fragment.
|
||||
'''
|
||||
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'),
|
||||
acme_version=dict(type='int', choices=[1, 2]),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
|
||||
)
|
||||
|
||||
|
||||
def create_backend(module, needs_acme_v2):
|
||||
backend = module.params['select_crypto_backend']
|
||||
|
||||
# Backend autodetect
|
||||
if backend == 'auto':
|
||||
backend = 'cryptography' if HAS_CURRENT_CRYPTOGRAPHY else 'openssl'
|
||||
|
||||
# Create backend object
|
||||
if backend == 'cryptography':
|
||||
if not HAS_CURRENT_CRYPTOGRAPHY:
|
||||
module.fail_json(msg=missing_required_lib('cryptography'))
|
||||
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
|
||||
module_backend = CryptographyBackend(module)
|
||||
elif backend == 'openssl':
|
||||
module.debug('Using OpenSSL binary backend')
|
||||
module_backend = OpenSSLCLIBackend(module)
|
||||
else:
|
||||
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
|
||||
|
||||
# Check common module parameters
|
||||
if not module.params['validate_certs']:
|
||||
module.warn(
|
||||
'Disabling certificate validation for communications with ACME endpoint. '
|
||||
'This should only be done for testing against a local ACME server for '
|
||||
'development purposes, but *never* for production purposes.'
|
||||
)
|
||||
|
||||
if module.params['acme_version'] is None:
|
||||
module.params['acme_version'] = 1
|
||||
module.deprecate("The option 'acme_version' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if module.params['acme_directory'] is None:
|
||||
module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory'
|
||||
module.deprecate("The option 'acme_directory' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if needs_acme_v2 and module.params['acme_version'] < 2:
|
||||
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
|
||||
|
||||
# AnsibleModule() changes the locale, so change it back to C because we rely
|
||||
# on datetime.datetime.strptime() when parsing certificate dates.
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
|
||||
return module_backend
|
||||
383
plugins/module_utils/acme/backend_cryptography.py
Normal file
383
plugins/module_utils/acme/backend_cryptography.py
Normal file
@@ -0,0 +1,383 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
|
||||
CryptoBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
|
||||
ChainMatcher,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
BackendException,
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
parse_name_field,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_name_to_oid,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
extract_first_pem,
|
||||
)
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.hazmat.primitives.hashes
|
||||
import cryptography.hazmat.primitives.hmac
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
import cryptography.hazmat.primitives.asymmetric.padding
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
import cryptography.hazmat.primitives.asymmetric.utils
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
CRYPTOGRAPHY_VERSION = cryptography.__version__
|
||||
HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion('1.5'))
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except Exception as dummy:
|
||||
HAS_CURRENT_CRYPTOGRAPHY = False
|
||||
CRYPTOGRAPHY_VERSION = None
|
||||
|
||||
|
||||
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):
|
||||
if key_identifier:
|
||||
try:
|
||||
return binascii.unhexlify(key_identifier.replace(':', ''))
|
||||
except Exception:
|
||||
if criterium_idx is None:
|
||||
module.warn('Criterium has invalid {0} value. Ignoring criterium.'.format(name))
|
||||
else:
|
||||
module.warn('Criterium {0} in select_chain has invalid {1} value. '
|
||||
'Ignoring criterium.'.format(criterium_idx, name))
|
||||
return None
|
||||
|
||||
def __init__(self, criterium, module):
|
||||
self.criterium = criterium
|
||||
self.test_certificates = criterium.test_certificates
|
||||
self.subject = []
|
||||
self.issuer = []
|
||||
if criterium.subject:
|
||||
self.subject = [
|
||||
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject)
|
||||
]
|
||||
if criterium.issuer:
|
||||
self.issuer = [
|
||||
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer)
|
||||
]
|
||||
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
|
||||
criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module)
|
||||
self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier(
|
||||
criterium.authority_key_identifier, 'authority_key_identifier', criterium.index, module)
|
||||
|
||||
def _match_subject(self, x509_subject, match_subject):
|
||||
for oid, value in match_subject:
|
||||
found = False
|
||||
for attribute in x509_subject:
|
||||
if attribute.oid == oid and value == to_native(attribute.value):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
return False
|
||||
return True
|
||||
|
||||
def match(self, certificate):
|
||||
'''
|
||||
Check whether an alternate chain matches the specified criterium.
|
||||
'''
|
||||
chain = certificate.chain
|
||||
if self.test_certificates == 'last':
|
||||
chain = chain[-1:]
|
||||
elif self.test_certificates == 'first':
|
||||
chain = chain[:1]
|
||||
for cert in chain:
|
||||
try:
|
||||
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend())
|
||||
matches = True
|
||||
if not self._match_subject(x509.subject, self.subject):
|
||||
matches = False
|
||||
if not self._match_subject(x509.issuer, self.issuer):
|
||||
matches = False
|
||||
if self.subject_key_identifier:
|
||||
try:
|
||||
ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier)
|
||||
if self.subject_key_identifier != ext.value.digest:
|
||||
matches = False
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
matches = False
|
||||
if self.authority_key_identifier:
|
||||
try:
|
||||
ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier)
|
||||
if self.authority_key_identifier != ext.value.key_identifier:
|
||||
matches = False
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
matches = False
|
||||
if matches:
|
||||
return True
|
||||
except Exception as e:
|
||||
self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e))
|
||||
return False
|
||||
|
||||
|
||||
class CryptographyBackend(CryptoBackend):
|
||||
def __init__(self, module):
|
||||
super(CryptographyBackend, self).__init__(module)
|
||||
|
||||
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.
|
||||
Raises KeyParsingError in case of errors.
|
||||
'''
|
||||
# If key_content isn't given, read key_file
|
||||
if key_content is None:
|
||||
key_content = read_file(key_file)
|
||||
else:
|
||||
key_content = to_bytes(key_content)
|
||||
# Parse key
|
||||
try:
|
||||
key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
key_content,
|
||||
password=to_bytes(passphrase) if passphrase is not None else None,
|
||||
backend=_cryptography_backend)
|
||||
except Exception as e:
|
||||
raise KeyParsingError('error while loading key: {0}'.format(e))
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
pk = key.public_key().public_numbers()
|
||||
return {
|
||||
'key_obj': key,
|
||||
'type': 'rsa',
|
||||
'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)),
|
||||
},
|
||||
'hash': 'sha256',
|
||||
}
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
pk = key.public_key().public_numbers()
|
||||
if pk.curve.name == 'secp256r1':
|
||||
bits = 256
|
||||
alg = 'ES256'
|
||||
hashalg = 'sha256'
|
||||
point_size = 32
|
||||
curve = 'P-256'
|
||||
elif pk.curve.name == 'secp384r1':
|
||||
bits = 384
|
||||
alg = 'ES384'
|
||||
hashalg = 'sha384'
|
||||
point_size = 48
|
||||
curve = 'P-384'
|
||||
elif pk.curve.name == 'secp521r1':
|
||||
# Not yet supported on Let's Encrypt side, see
|
||||
# https://github.com/letsencrypt/boulder/issues/2217
|
||||
bits = 521
|
||||
alg = 'ES512'
|
||||
hashalg = 'sha512'
|
||||
point_size = 66
|
||||
curve = 'P-521'
|
||||
else:
|
||||
raise KeyParsingError('unknown elliptic curve: {0}'.format(pk.curve.name))
|
||||
num_bytes = (bits + 7) // 8
|
||||
return {
|
||||
'key_obj': key,
|
||||
'type': 'ec',
|
||||
'alg': alg,
|
||||
'jwk': {
|
||||
"kty": "EC",
|
||||
"crv": curve,
|
||||
"x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)),
|
||||
"y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)),
|
||||
},
|
||||
'hash': hashalg,
|
||||
'point_size': point_size,
|
||||
}
|
||||
else:
|
||||
raise KeyParsingError('unknown key type "{0}"'.format(type(key)))
|
||||
|
||||
def sign(self, payload64, protected64, key_data):
|
||||
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||
if 'mac_obj' in key_data:
|
||||
mac = key_data['mac_obj']()
|
||||
mac.update(sign_payload)
|
||||
signature = mac.finalize()
|
||||
elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA256
|
||||
signature = key_data['key_obj'].sign(sign_payload, padding, hashalg())
|
||||
elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
if key_data['hash'] == 'sha256':
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA256
|
||||
elif key_data['hash'] == 'sha384':
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA384
|
||||
elif key_data['hash'] == 'sha512':
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA512
|
||||
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg())
|
||||
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa))
|
||||
rr = _pad_hex(r, 2 * key_data['point_size'])
|
||||
ss = _pad_hex(s, 2 * key_data['point_size'])
|
||||
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
|
||||
|
||||
return {
|
||||
"protected": protected64,
|
||||
"payload": payload64,
|
||||
"signature": nopad_b64(signature),
|
||||
}
|
||||
|
||||
def create_mac_key(self, alg, key):
|
||||
'''Create a MAC key.'''
|
||||
if alg == 'HS256':
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA256
|
||||
hashbytes = 32
|
||||
elif alg == 'HS384':
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA384
|
||||
hashbytes = 48
|
||||
elif alg == 'HS512':
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA512
|
||||
hashbytes = 64
|
||||
else:
|
||||
raise BackendException('Unsupported MAC key algorithm for cryptography backend: {0}'.format(alg))
|
||||
key_bytes = base64.urlsafe_b64decode(key)
|
||||
if len(key_bytes) < hashbytes:
|
||||
raise BackendException(
|
||||
'{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes))
|
||||
return {
|
||||
'mac_obj': lambda: cryptography.hazmat.primitives.hmac.HMAC(
|
||||
key_bytes,
|
||||
hashalg(),
|
||||
_cryptography_backend),
|
||||
'type': 'hmac',
|
||||
'alg': alg,
|
||||
'jwk': {
|
||||
'kty': 'oct',
|
||||
'k': key,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
|
||||
'''
|
||||
Return the days the certificate in cert_filename remains valid and -1
|
||||
if the file was not found. If cert_filename contains more than one
|
||||
certificate, only the first one will be considered.
|
||||
|
||||
If now is not specified, datetime.datetime.now() is used.
|
||||
'''
|
||||
if cert_filename is not None:
|
||||
cert_content = None
|
||||
if os.path.exists(cert_filename):
|
||||
cert_content = read_file(cert_filename)
|
||||
else:
|
||||
cert_content = to_bytes(cert_content)
|
||||
|
||||
if cert_content is None:
|
||||
return -1
|
||||
|
||||
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
|
||||
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))
|
||||
|
||||
if now is None:
|
||||
now = datetime.datetime.now()
|
||||
return (cert.not_valid_after - now).days
|
||||
|
||||
def create_chain_matcher(self, criterium):
|
||||
'''
|
||||
Given a Criterium object, creates a ChainMatcher object.
|
||||
'''
|
||||
return CryptographyChainMatcher(criterium, self.module)
|
||||
298
plugins/module_utils/acme/backend_openssl_cli.py
Normal file
298
plugins/module_utils/acme/backend_openssl_cli.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
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 (
|
||||
CryptoBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
BackendException,
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
|
||||
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
|
||||
|
||||
class OpenSSLCLIBackend(CryptoBackend):
|
||||
def __init__(self, module, openssl_binary=None):
|
||||
super(OpenSSLCLIBackend, self).__init__(module)
|
||||
if openssl_binary is None:
|
||||
openssl_binary = module.get_bin_path('openssl', True)
|
||||
self.openssl_binary = openssl_binary
|
||||
|
||||
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.
|
||||
Raises KeyParsingError in case of errors.
|
||||
'''
|
||||
if passphrase is not None:
|
||||
raise KeyParsingError('openssl backend does not support key passphrases')
|
||||
# If key_file isn't given, but key_content, write that to a temporary file
|
||||
if key_file is None:
|
||||
fd, tmpsrc = tempfile.mkstemp()
|
||||
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
f.write(key_content.encode('utf-8'))
|
||||
key_file = tmpsrc
|
||||
except Exception as err:
|
||||
try:
|
||||
f.close()
|
||||
except Exception as dummy:
|
||||
pass
|
||||
raise KeyParsingError("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||
f.close()
|
||||
# Parse key
|
||||
account_key_type = None
|
||||
with open(key_file, "rt") as f:
|
||||
for line in f:
|
||||
m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line)
|
||||
if m is not None:
|
||||
account_key_type = m.group(1).lower()
|
||||
break
|
||||
if account_key_type is None:
|
||||
# This happens for example if openssl_privatekey created this key
|
||||
# (as opposed to the OpenSSL binary). For now, we assume this is
|
||||
# an RSA key.
|
||||
# FIXME: add some kind of auto-detection
|
||||
account_key_type = "rsa"
|
||||
if account_key_type not in ("rsa", "ec"):
|
||||
raise KeyParsingError('unknown key type "%s"' % account_key_type)
|
||||
|
||||
openssl_keydump_cmd = [self.openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"]
|
||||
dummy, out, dummy = self.module.run_command(
|
||||
openssl_keydump_cmd, check_rc=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||
|
||||
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_exp = "{0:x}".format(int(pub_exp))
|
||||
if len(pub_exp) % 2:
|
||||
pub_exp = "0{0}".format(pub_exp)
|
||||
|
||||
return {
|
||||
'key_file': key_file,
|
||||
'type': 'rsa',
|
||||
'alg': 'RS256',
|
||||
'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"))),
|
||||
},
|
||||
'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)
|
||||
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"))
|
||||
asn1_oid_curve = pub_data.group(2).lower()
|
||||
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
|
||||
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
|
||||
bits = 256
|
||||
alg = 'ES256'
|
||||
hashalg = 'sha256'
|
||||
point_size = 32
|
||||
curve = 'P-256'
|
||||
elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
|
||||
bits = 384
|
||||
alg = 'ES384'
|
||||
hashalg = 'sha384'
|
||||
point_size = 48
|
||||
curve = 'P-384'
|
||||
elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
|
||||
# Not yet supported on Let's Encrypt side, see
|
||||
# https://github.com/letsencrypt/boulder/issues/2217
|
||||
bits = 521
|
||||
alg = 'ES512'
|
||||
hashalg = 'sha512'
|
||||
point_size = 66
|
||||
curve = 'P-521'
|
||||
else:
|
||||
raise KeyParsingError('unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve))
|
||||
num_bytes = (bits + 7) // 8
|
||||
if len(pub_hex) != 2 * num_bytes:
|
||||
raise KeyParsingError('bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve))
|
||||
return {
|
||||
'key_file': key_file,
|
||||
'type': 'ec',
|
||||
'alg': alg,
|
||||
'jwk': {
|
||||
"kty": "EC",
|
||||
"crv": curve,
|
||||
"x": nopad_b64(pub_hex[:num_bytes]),
|
||||
"y": nopad_b64(pub_hex[num_bytes:]),
|
||||
},
|
||||
'hash': hashalg,
|
||||
'point_size': point_size,
|
||||
}
|
||||
|
||||
def sign(self, payload64, protected64, key_data):
|
||||
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||
if key_data['type'] == 'hmac':
|
||||
hex_key = to_native(binascii.hexlify(base64.urlsafe_b64decode(key_data['jwk']['k'])))
|
||||
cmd_postfix = ["-mac", "hmac", "-macopt", "hexkey:{0}".format(hex_key), "-binary"]
|
||||
else:
|
||||
cmd_postfix = ["-sign", key_data['key_file']]
|
||||
openssl_sign_cmd = [self.openssl_binary, "dgst", "-{0}".format(key_data['hash'])] + cmd_postfix
|
||||
|
||||
dummy, out, dummy = self.module.run_command(
|
||||
openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||
|
||||
if key_data['type'] == 'ec':
|
||||
dummy, der_out, dummy = self.module.run_command(
|
||||
[self.openssl_binary, "asn1parse", "-inform", "DER"],
|
||||
data=out, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||
expected_len = 2 * key_data['point_size']
|
||||
sig = re.findall(
|
||||
r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
|
||||
to_text(der_out, errors='surrogate_or_strict'))
|
||||
if len(sig) != 2:
|
||||
raise BackendException(
|
||||
"failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
|
||||
to_text(der_out, errors='surrogate_or_strict')))
|
||||
sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
|
||||
sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
|
||||
out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1])
|
||||
|
||||
return {
|
||||
"protected": protected64,
|
||||
"payload": payload64,
|
||||
"signature": nopad_b64(to_bytes(out)),
|
||||
}
|
||||
|
||||
def create_mac_key(self, alg, key):
|
||||
'''Create a MAC key.'''
|
||||
if alg == 'HS256':
|
||||
hashalg = 'sha256'
|
||||
hashbytes = 32
|
||||
elif alg == 'HS384':
|
||||
hashalg = 'sha384'
|
||||
hashbytes = 48
|
||||
elif alg == 'HS512':
|
||||
hashalg = 'sha512'
|
||||
hashbytes = 64
|
||||
else:
|
||||
raise BackendException('Unsupported MAC key algorithm for OpenSSL backend: {0}'.format(alg))
|
||||
key_bytes = base64.urlsafe_b64decode(key)
|
||||
if len(key_bytes) < hashbytes:
|
||||
raise BackendException(
|
||||
'{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes))
|
||||
return {
|
||||
'type': 'hmac',
|
||||
'alg': alg,
|
||||
'jwk': {
|
||||
'kty': 'oct',
|
||||
'k': key,
|
||||
},
|
||||
'hash': hashalg,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_ip(ip):
|
||||
try:
|
||||
return to_native(compat_ipaddress.ip_address(to_text(ip)).compressed)
|
||||
except ValueError:
|
||||
# We don't want to error out on something IPAddress() can't parse
|
||||
return ip
|
||||
|
||||
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'.
|
||||
'''
|
||||
filename = csr_filename
|
||||
data = None
|
||||
if csr_content is not None:
|
||||
filename = '/dev/stdin'
|
||||
data = csr_content.encode('utf-8')
|
||||
|
||||
openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"]
|
||||
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([])
|
||||
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)))
|
||||
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:]))
|
||||
elif san.lower().startswith("ip:"):
|
||||
identifiers.add(('ip', self._normalize_ip(san[3:])))
|
||||
elif san.lower().startswith("ip address:"):
|
||||
identifiers.add(('ip', self._normalize_ip(san[11:])))
|
||||
else:
|
||||
raise BackendException('Found unsupported SAN identifier "{0}"'.format(san))
|
||||
return identifiers
|
||||
|
||||
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
|
||||
'''
|
||||
Return the days the certificate in cert_filename remains valid and -1
|
||||
if the file was not found. If cert_filename contains more than one
|
||||
certificate, only the first one will be considered.
|
||||
|
||||
If now is not specified, datetime.datetime.now() is used.
|
||||
'''
|
||||
filename = cert_filename
|
||||
data = None
|
||||
if cert_content is not None:
|
||||
filename = '/dev/stdin'
|
||||
data = cert_content.encode('utf-8')
|
||||
cert_filename_suffix = ''
|
||||
elif cert_filename is not None:
|
||||
if not os.path.exists(cert_filename):
|
||||
return -1
|
||||
cert_filename_suffix = ' in {0}'.format(cert_filename)
|
||||
else:
|
||||
return -1
|
||||
|
||||
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))
|
||||
if now is None:
|
||||
now = datetime.datetime.now()
|
||||
return (not_after - now).days
|
||||
|
||||
def create_chain_matcher(self, criterium):
|
||||
'''
|
||||
Given a Criterium object, creates a ChainMatcher object.
|
||||
'''
|
||||
raise BackendException('Alternate chain matching can only be used with the "cryptography" backend.')
|
||||
58
plugins/module_utils/acme/backends.py
Normal file
58
plugins/module_utils/acme/backends.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
from ansible.module_utils import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CryptoBackend(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
@abc.abstractmethod
|
||||
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.
|
||||
Raises KeyParsingError in case of errors.
|
||||
'''
|
||||
|
||||
@abc.abstractmethod
|
||||
def sign(self, payload64, protected64, key_data):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_mac_key(self, alg, key):
|
||||
'''Create a MAC key.'''
|
||||
|
||||
@abc.abstractmethod
|
||||
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'.
|
||||
'''
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
|
||||
'''
|
||||
Return the days the certificate in cert_filename remains valid and -1
|
||||
if the file was not found. If cert_filename contains more than one
|
||||
certificate, only the first one will be considered.
|
||||
|
||||
If now is not specified, datetime.datetime.now() is used.
|
||||
'''
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_chain_matcher(self, criterium):
|
||||
'''
|
||||
Given a Criterium object, creates a ChainMatcher object.
|
||||
'''
|
||||
128
plugins/module_utils/acme/certificates.py
Normal file
128
plugins/module_utils/acme/certificates.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
from ansible.module_utils import six
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
ModuleFailException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
der_to_pem,
|
||||
nopad_b64,
|
||||
process_links,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
|
||||
|
||||
class CertificateChain(object):
|
||||
'''
|
||||
Download and parse the certificate chain.
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4.2
|
||||
'''
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
self.cert = None
|
||||
self.chain = []
|
||||
self.alternates = []
|
||||
|
||||
@classmethod
|
||||
def download(cls, client, url):
|
||||
content, info = client.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'})
|
||||
|
||||
if not content or not info['content-type'].startswith('application/pem-certificate-chain'):
|
||||
raise ModuleFailException(
|
||||
"Cannot download certificate chain from {0}, as content type is not application/pem-certificate-chain: {1} (headers: {2})".format(
|
||||
url, content, info))
|
||||
|
||||
result = cls(url)
|
||||
|
||||
# Parse data
|
||||
certs = split_pem_list(content.decode('utf-8'), keep_inbetween=True)
|
||||
if certs:
|
||||
result.cert = certs[0]
|
||||
result.chain = certs[1:]
|
||||
|
||||
process_links(info, lambda link, relation: result._process_links(client, link, relation))
|
||||
|
||||
if result.cert is None:
|
||||
raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info))
|
||||
|
||||
return result
|
||||
|
||||
def _process_links(self, client, link, relation):
|
||||
if relation == 'up':
|
||||
# Process link-up headers if there was no chain in reply
|
||||
if not self.chain:
|
||||
chain_result, chain_info = client.get_request(link, parse_json_result=False)
|
||||
if chain_info['status'] in [200, 201]:
|
||||
self.chain.append(der_to_pem(chain_result))
|
||||
elif relation == 'alternate':
|
||||
self.alternates.append(link)
|
||||
|
||||
def to_json(self):
|
||||
cert = self.cert.encode('utf8')
|
||||
chain = ('\n'.join(self.chain)).encode('utf8')
|
||||
return {
|
||||
'cert': cert,
|
||||
'chain': chain,
|
||||
'full_chain': cert + chain,
|
||||
}
|
||||
|
||||
|
||||
class Criterium(object):
|
||||
def __init__(self, criterium, index=None):
|
||||
self.index = index
|
||||
self.test_certificates = criterium['test_certificates']
|
||||
self.subject = criterium['subject']
|
||||
self.issuer = criterium['issuer']
|
||||
self.subject_key_identifier = criterium['subject_key_identifier']
|
||||
self.authority_key_identifier = criterium['authority_key_identifier']
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ChainMatcher(object):
|
||||
@abc.abstractmethod
|
||||
def match(self, certificate):
|
||||
'''
|
||||
Check whether a certificate chain (CertificateChain instance) matches.
|
||||
'''
|
||||
|
||||
|
||||
def retrieve_acme_v1_certificate(client, csr_der):
|
||||
'''
|
||||
Create a new certificate based on the CSR (ACME v1 protocol).
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
|
||||
'''
|
||||
new_cert = {
|
||||
"resource": "new-cert",
|
||||
"csr": nopad_b64(csr_der),
|
||||
}
|
||||
result, info = client.send_signed_request(
|
||||
client.directory['new-cert'], new_cert, error_msg='Failed to receive certificate', expected_status_codes=[200, 201])
|
||||
cert = CertificateChain(info['location'])
|
||||
cert.cert = der_to_pem(result)
|
||||
|
||||
def f(link, relation):
|
||||
if relation == 'up':
|
||||
chain_result, chain_info = client.get_request(link, parse_json_result=False)
|
||||
if chain_info['status'] in [200, 201]:
|
||||
del cert.chain[:]
|
||||
cert.chain.append(der_to_pem(chain_result))
|
||||
|
||||
process_links(info, f)
|
||||
return cert
|
||||
299
plugins/module_utils/acme/challenges.py
Normal file
299
plugins/module_utils/acme/challenges.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
format_error_problem,
|
||||
ACMEProtocolException,
|
||||
ModuleFailException,
|
||||
)
|
||||
|
||||
|
||||
def create_key_authorization(client, token):
|
||||
'''
|
||||
Returns the key authorization for the given token
|
||||
https://tools.ietf.org/html/rfc8555#section-8.1
|
||||
'''
|
||||
accountkey_json = json.dumps(client.account_jwk, sort_keys=True, separators=(',', ':'))
|
||||
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
|
||||
return "{0}.{1}".format(token, thumbprint)
|
||||
|
||||
|
||||
def combine_identifier(identifier_type, identifier):
|
||||
return '{type}:{identifier}'.format(type=identifier_type, identifier=identifier)
|
||||
|
||||
|
||||
def split_identifier(identifier):
|
||||
parts = identifier.split(':', 1)
|
||||
if len(parts) != 2:
|
||||
raise ModuleFailException(
|
||||
'Identifier "{identifier}" is not of the form <type>:<identifier>'.format(identifier=identifier))
|
||||
return parts
|
||||
|
||||
|
||||
class Challenge(object):
|
||||
def __init__(self, data, url):
|
||||
self.data = data
|
||||
|
||||
self.type = data['type']
|
||||
self.url = url
|
||||
self.status = data['status']
|
||||
self.token = data.get('token')
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, client, data, url=None):
|
||||
return cls(data, url or (data['uri'] if client.version == 1 else data['url']))
|
||||
|
||||
def call_validate(self, client):
|
||||
challenge_response = {}
|
||||
if client.version == 1:
|
||||
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
|
||||
key_authorization = create_key_authorization(client, token)
|
||||
challenge_response['resource'] = 'challenge'
|
||||
challenge_response['keyAuthorization'] = key_authorization
|
||||
challenge_response['type'] = self.type
|
||||
client.send_signed_request(
|
||||
self.url,
|
||||
challenge_response,
|
||||
error_msg='Failed to validate challenge',
|
||||
expected_status_codes=[200, 202],
|
||||
)
|
||||
|
||||
def to_json(self):
|
||||
return self.data.copy()
|
||||
|
||||
def get_validation_data(self, client, identifier_type, identifier):
|
||||
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
|
||||
key_authorization = create_key_authorization(client, token)
|
||||
|
||||
if self.type == 'http-01':
|
||||
# https://tools.ietf.org/html/rfc8555#section-8.3
|
||||
return {
|
||||
'resource': '.well-known/acme-challenge/{token}'.format(token=token),
|
||||
'resource_value': key_authorization,
|
||||
}
|
||||
|
||||
if self.type == 'dns-01':
|
||||
if identifier_type != 'dns':
|
||||
return None
|
||||
# https://tools.ietf.org/html/rfc8555#section-8.4
|
||||
resource = '_acme-challenge'
|
||||
value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest())
|
||||
record = (resource + identifier[1:]) if identifier.startswith('*.') else '{0}.{1}'.format(resource, identifier)
|
||||
return {
|
||||
'resource': resource,
|
||||
'resource_value': value,
|
||||
'record': record,
|
||||
}
|
||||
|
||||
if self.type == 'tls-alpn-01':
|
||||
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3
|
||||
if identifier_type == 'ip':
|
||||
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
|
||||
resource = compat_ipaddress.ip_address(identifier).reverse_pointer
|
||||
if not resource.endswith('.'):
|
||||
resource += '.'
|
||||
else:
|
||||
resource = identifier
|
||||
value = base64.b64encode(hashlib.sha256(to_bytes(key_authorization)).digest())
|
||||
return {
|
||||
'resource': resource,
|
||||
'resource_original': combine_identifier(identifier_type, identifier),
|
||||
'resource_value': value,
|
||||
}
|
||||
|
||||
# Unknown challenge type: ignore
|
||||
return None
|
||||
|
||||
|
||||
class Authorization(object):
|
||||
def _setup(self, client, data):
|
||||
data['uri'] = self.url
|
||||
self.data = data
|
||||
self.challenges = [Challenge.from_json(client, challenge) for challenge in data['challenges']]
|
||||
if client.version == 1 and 'status' not in data:
|
||||
# https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
|
||||
# "status (required, string): ...
|
||||
# If this field is missing, then the default value is "pending"."
|
||||
self.status = 'pending'
|
||||
else:
|
||||
self.status = data['status']
|
||||
self.identifier = data['identifier']['value']
|
||||
self.identifier_type = data['identifier']['type']
|
||||
if data.get('wildcard', False):
|
||||
self.identifier = '*.{0}'.format(self.identifier)
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
self.data = None
|
||||
self.challenges = []
|
||||
self.status = None
|
||||
self.identifier_type = None
|
||||
self.identifier = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, client, data, url):
|
||||
result = cls(url)
|
||||
result._setup(client, data)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_url(cls, client, url):
|
||||
result = cls(url)
|
||||
result.refresh(client)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def create(cls, client, identifier_type, identifier):
|
||||
'''
|
||||
Create a new authorization for the given identifier.
|
||||
Return the authorization object of the new authorization
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
|
||||
'''
|
||||
new_authz = {
|
||||
"identifier": {
|
||||
"type": identifier_type,
|
||||
"value": identifier,
|
||||
},
|
||||
}
|
||||
if client.version == 1:
|
||||
url = client.directory['new-authz']
|
||||
new_authz["resource"] = "new-authz"
|
||||
else:
|
||||
if 'newAuthz' not in client.directory.directory:
|
||||
raise ACMEProtocolException(client.module, 'ACME endpoint does not support pre-authorization')
|
||||
url = client.directory['newAuthz']
|
||||
|
||||
result, info = client.send_signed_request(
|
||||
url, new_authz, error_msg='Failed to request challenges', expected_status_codes=[200, 201])
|
||||
return cls.from_json(client, result, info['location'])
|
||||
|
||||
@property
|
||||
def combined_identifier(self):
|
||||
return combine_identifier(self.identifier_type, self.identifier)
|
||||
|
||||
def to_json(self):
|
||||
return self.data.copy()
|
||||
|
||||
def refresh(self, client):
|
||||
result, dummy = client.get_request(self.url)
|
||||
changed = self.data != result
|
||||
self._setup(client, result)
|
||||
return changed
|
||||
|
||||
def get_challenge_data(self, client):
|
||||
'''
|
||||
Returns a dict with the data for all proposed (and supported) challenges
|
||||
of the given authorization.
|
||||
'''
|
||||
data = {}
|
||||
for challenge in self.challenges:
|
||||
validation_data = challenge.get_validation_data(client, self.identifier_type, self.identifier)
|
||||
if validation_data is not None:
|
||||
data[challenge.type] = validation_data
|
||||
return data
|
||||
|
||||
def raise_error(self, error_msg, module=None):
|
||||
'''
|
||||
Aborts with a specific error for a challenge.
|
||||
'''
|
||||
error_details = []
|
||||
# multiple challenges could have failed at this point, gather error
|
||||
# details for all of them before failing
|
||||
for challenge in self.challenges:
|
||||
if challenge.status == 'invalid':
|
||||
msg = 'Challenge {type}'.format(type=challenge.type)
|
||||
if 'error' in challenge.data:
|
||||
msg = '{msg}: {problem}'.format(
|
||||
msg=msg,
|
||||
problem=format_error_problem(challenge.data['error'], subproblem_prefix='{0}.'.format(challenge.type)),
|
||||
)
|
||||
error_details.append(msg)
|
||||
raise ACMEProtocolException(
|
||||
module,
|
||||
'Failed to validate challenge for {identifier}: {error}. {details}'.format(
|
||||
identifier=self.combined_identifier,
|
||||
error=error_msg,
|
||||
details='; '.join(error_details),
|
||||
),
|
||||
extras=dict(
|
||||
identifier=self.combined_identifier,
|
||||
authorization=self.data,
|
||||
),
|
||||
)
|
||||
|
||||
def find_challenge(self, challenge_type):
|
||||
for challenge in self.challenges:
|
||||
if challenge_type == challenge.type:
|
||||
return challenge
|
||||
return None
|
||||
|
||||
def wait_for_validation(self, client, callenge_type):
|
||||
while True:
|
||||
self.refresh(client)
|
||||
if self.status in ['valid', 'invalid', 'revoked']:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
if self.status == 'invalid':
|
||||
self.raise_error('Status is "invalid"', module=client.module)
|
||||
|
||||
return self.status == 'valid'
|
||||
|
||||
def call_validate(self, client, challenge_type, wait=True):
|
||||
'''
|
||||
Validate the authorization provided in the auth dict. Returns True
|
||||
when the validation was successful and False when it was not.
|
||||
'''
|
||||
challenge = self.find_challenge(challenge_type)
|
||||
if challenge is None:
|
||||
raise ModuleFailException('Found no challenge of type "{challenge}" for identifier {identifier}!'.format(
|
||||
challenge=challenge_type,
|
||||
identifier=self.combined_identifier,
|
||||
))
|
||||
|
||||
challenge.call_validate(client)
|
||||
|
||||
if not wait:
|
||||
return self.status == 'valid'
|
||||
return self.wait_for_validation(client, challenge_type)
|
||||
|
||||
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':
|
||||
return
|
||||
authz_deactivate = {
|
||||
'status': 'deactivated'
|
||||
}
|
||||
if client.version == 1:
|
||||
authz_deactivate['resource'] = 'authz'
|
||||
result, info = client.send_signed_request(self.url, authz_deactivate, fail_on_error=False)
|
||||
if 200 <= info['status'] < 300 and result.get('status') == 'deactivated':
|
||||
self.status = 'deactivated'
|
||||
return True
|
||||
return False
|
||||
135
plugins/module_utils/acme/errors.py
Normal file
135
plugins/module_utils/acme/errors.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.six import binary_type, PY3
|
||||
|
||||
|
||||
def format_error_problem(problem, subproblem_prefix=''):
|
||||
if 'title' in problem:
|
||||
msg = 'Error "{title}" ({type})'.format(
|
||||
type=problem['type'],
|
||||
title=problem['title'],
|
||||
)
|
||||
else:
|
||||
msg = 'Error {type}'.format(type=problem['type'])
|
||||
if 'detail' in problem:
|
||||
msg += ': "{detail}"'.format(detail=problem['detail'])
|
||||
subproblems = problem.get('subproblems')
|
||||
if subproblems is not None:
|
||||
msg = '{msg} Subproblems:'.format(msg=msg)
|
||||
for index, problem in enumerate(subproblems):
|
||||
index_str = '{prefix}{index}'.format(prefix=subproblem_prefix, index=index)
|
||||
msg = '{msg}\n({index}) {problem}'.format(
|
||||
msg=msg,
|
||||
index=index_str,
|
||||
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index_str)),
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
class ModuleFailException(Exception):
|
||||
'''
|
||||
If raised, module.fail_json() will be called with the given parameters after cleanup.
|
||||
'''
|
||||
def __init__(self, msg, **args):
|
||||
super(ModuleFailException, self).__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.module_fail_args = args
|
||||
|
||||
def do_fail(self, module, **arguments):
|
||||
module.fail_json(msg=self.msg, other=self.module_fail_args, **arguments)
|
||||
|
||||
|
||||
class ACMEProtocolException(ModuleFailException):
|
||||
def __init__(self, module, msg=None, info=None, response=None, content=None, content_json=None, extras=None):
|
||||
# Try to get hold of content, if response is given and content is not provided
|
||||
if content is None and content_json is None and response is not None:
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
# In Python 3, read() simply returns ''
|
||||
if PY3 and response.closed:
|
||||
raise TypeError
|
||||
content = response.read()
|
||||
except (AttributeError, TypeError):
|
||||
content = info.pop('body', None)
|
||||
|
||||
# Make sure that content_json is None or a dictionary
|
||||
if content_json is not None and not isinstance(content_json, dict):
|
||||
if content is None and isinstance(content_json, binary_type):
|
||||
content = content_json
|
||||
content_json = None
|
||||
|
||||
# Try to get hold of JSON decoded content, when content is given and JSON not provided
|
||||
if content_json is None and content is not None and module is not None:
|
||||
try:
|
||||
content_json = module.from_json(to_text(content))
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
extras = extras or dict()
|
||||
|
||||
if msg is None:
|
||||
msg = 'ACME request failed'
|
||||
add_msg = ''
|
||||
|
||||
if info is not None:
|
||||
url = info['url']
|
||||
code = info['status']
|
||||
extras['http_url'] = url
|
||||
extras['http_status'] = code
|
||||
if code is not None and code >= 400 and content_json is not None and 'type' in content_json:
|
||||
if 'status' in content_json and content_json['status'] != code:
|
||||
code = 'status {problem_code} (HTTP status: {http_code})'.format(http_code=code, problem_code=content_json['status'])
|
||||
else:
|
||||
code = 'status {problem_code}'.format(problem_code=code)
|
||||
subproblems = content_json.pop('subproblems', None)
|
||||
add_msg = ' {problem}.'.format(problem=format_error_problem(content_json))
|
||||
extras['problem'] = content_json
|
||||
extras['subproblems'] = subproblems or []
|
||||
if subproblems is not None:
|
||||
add_msg = '{add_msg} Subproblems:'.format(add_msg=add_msg)
|
||||
for index, problem in enumerate(subproblems):
|
||||
add_msg = '{add_msg}\n({index}) {problem}.'.format(
|
||||
add_msg=add_msg,
|
||||
index=index,
|
||||
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)),
|
||||
)
|
||||
else:
|
||||
code = 'HTTP status {code}'.format(code=code)
|
||||
if content_json is not None:
|
||||
add_msg = ' The JSON error result: {content}'.format(content=content_json)
|
||||
elif content is not None:
|
||||
add_msg = ' The raw error result: {content}'.format(content=to_text(content))
|
||||
msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=code)
|
||||
elif content_json is not None:
|
||||
add_msg = ' The JSON result: {content}'.format(content=content_json)
|
||||
elif content is not None:
|
||||
add_msg = ' The raw result: {content}'.format(content=to_text(content))
|
||||
|
||||
super(ACMEProtocolException, self).__init__(
|
||||
'{msg}.{add_msg}'.format(msg=msg, add_msg=add_msg),
|
||||
**extras
|
||||
)
|
||||
self.problem = {}
|
||||
self.subproblems = []
|
||||
for k, v in extras.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class BackendException(ModuleFailException):
|
||||
pass
|
||||
|
||||
|
||||
class NetworkException(ModuleFailException):
|
||||
pass
|
||||
|
||||
|
||||
class KeyParsingError(ModuleFailException):
|
||||
pass
|
||||
86
plugins/module_utils/acme/io.py
Normal file
86
plugins/module_utils/acme/io.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2013, Romeo Theriault <romeot () hawaii.edu>
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
|
||||
def read_file(fn, mode='b'):
|
||||
try:
|
||||
with open(fn, 'r' + mode) as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e))
|
||||
|
||||
|
||||
# This function was adapted from an earlier version of https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py
|
||||
def write_file(module, dest, content):
|
||||
'''
|
||||
Write content to destination file dest, only if the content
|
||||
has changed.
|
||||
'''
|
||||
changed = False
|
||||
# create a tempfile
|
||||
fd, tmpsrc = tempfile.mkstemp(text=False)
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
f.write(content)
|
||||
except Exception as err:
|
||||
try:
|
||||
f.close()
|
||||
except Exception as dummy:
|
||||
pass
|
||||
os.remove(tmpsrc)
|
||||
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||
f.close()
|
||||
checksum_src = None
|
||||
checksum_dest = None
|
||||
# raise an error if there is no tmpsrc file
|
||||
if not os.path.exists(tmpsrc):
|
||||
try:
|
||||
os.remove(tmpsrc)
|
||||
except Exception as dummy:
|
||||
pass
|
||||
raise ModuleFailException("Source %s does not exist" % (tmpsrc))
|
||||
if not os.access(tmpsrc, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
raise ModuleFailException("Source %s not readable" % (tmpsrc))
|
||||
checksum_src = module.sha1(tmpsrc)
|
||||
# check if there is no dest file
|
||||
if os.path.exists(dest):
|
||||
# raise an error if copy has no permission on dest
|
||||
if not os.access(dest, os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
raise ModuleFailException("Destination %s not writable" % (dest))
|
||||
if not os.access(dest, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
raise ModuleFailException("Destination %s not readable" % (dest))
|
||||
checksum_dest = module.sha1(dest)
|
||||
else:
|
||||
dirname = os.path.dirname(dest) or '.'
|
||||
if not os.access(dirname, os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
raise ModuleFailException("Destination dir %s not writable" % (dirname))
|
||||
if checksum_src != checksum_dest:
|
||||
try:
|
||||
shutil.copyfile(tmpsrc, dest)
|
||||
changed = True
|
||||
except Exception as err:
|
||||
os.remove(tmpsrc)
|
||||
raise ModuleFailException("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc())
|
||||
os.remove(tmpsrc)
|
||||
return changed
|
||||
129
plugins/module_utils/acme/orders.py
Normal file
129
plugins/module_utils/acme/orders.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import time
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
ACMEProtocolException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
|
||||
Authorization,
|
||||
)
|
||||
|
||||
|
||||
class Order(object):
|
||||
def _setup(self, client, data):
|
||||
self.data = data
|
||||
|
||||
self.status = data['status']
|
||||
self.identifiers = []
|
||||
for identifier in data['identifiers']:
|
||||
self.identifiers.append((identifier['type'], identifier['value']))
|
||||
self.finalize_uri = data.get('finalize')
|
||||
self.certificate_uri = data.get('certificate')
|
||||
self.authorization_uris = data['authorizations']
|
||||
self.authorizations = {}
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
self.data = None
|
||||
|
||||
self.status = None
|
||||
self.identifiers = []
|
||||
self.finalize_uri = None
|
||||
self.certificate_uri = None
|
||||
self.authorization_uris = []
|
||||
self.authorizations = {}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, client, data, url):
|
||||
result = cls(url)
|
||||
result._setup(client, data)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_url(cls, client, url):
|
||||
result = cls(url)
|
||||
result.refresh(client)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def create(cls, client, identifiers):
|
||||
'''
|
||||
Start a new certificate order (ACME v2 protocol).
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
'''
|
||||
acme_identifiers = []
|
||||
for identifier_type, identifier in identifiers:
|
||||
acme_identifiers.append({
|
||||
'type': identifier_type,
|
||||
'value': identifier,
|
||||
})
|
||||
new_order = {
|
||||
"identifiers": acme_identifiers
|
||||
}
|
||||
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'])
|
||||
|
||||
def refresh(self, client):
|
||||
result, dummy = client.get_request(self.url)
|
||||
changed = self.data != result
|
||||
self._setup(client, result)
|
||||
return changed
|
||||
|
||||
def load_authorizations(self, client):
|
||||
for auth_uri in self.authorization_uris:
|
||||
authz = Authorization.from_url(client, auth_uri)
|
||||
self.authorizations[authz.combined_identifier] = authz
|
||||
|
||||
def wait_for_finalization(self, client):
|
||||
while True:
|
||||
self.refresh(client)
|
||||
if self.status in ['valid', 'invalid', 'pending', 'ready']:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
if self.status != 'valid':
|
||||
raise ACMEProtocolException(
|
||||
client.module,
|
||||
'Failed to wait for order to complete; got status "{status}"'.format(status=self.status),
|
||||
content_json=self.data)
|
||||
|
||||
def finalize(self, client, csr_der, wait=True):
|
||||
'''
|
||||
Create a new certificate based on the csr.
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
'''
|
||||
new_cert = {
|
||||
"csr": nopad_b64(csr_der),
|
||||
}
|
||||
result, info = client.send_signed_request(
|
||||
self.finalize_uri, new_cert, error_msg='Failed to finalizing order', expected_status_codes=[200])
|
||||
# It is not clear from the RFC whether the finalize call returns the order object or not.
|
||||
# Instead of using the result, we call self.refresh(client) below.
|
||||
|
||||
if wait:
|
||||
self.wait_for_finalization(client)
|
||||
else:
|
||||
self.refresh(client)
|
||||
if self.status not in ['procesing', 'valid', 'invalid']:
|
||||
raise ACMEProtocolException(
|
||||
client.module,
|
||||
'Failed to finalize order; got status "{status}"'.format(status=self.status),
|
||||
info=info,
|
||||
content_json=result)
|
||||
71
plugins/module_utils/acme/utils.py
Normal file
71
plugins/module_utils/acme/utils.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import re
|
||||
import textwrap
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.six.moves.urllib.parse import unquote
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
|
||||
def nopad_b64(data):
|
||||
return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "")
|
||||
|
||||
|
||||
def der_to_pem(der_cert):
|
||||
'''
|
||||
Convert the DER format certificate in der_cert to a PEM format certificate and return it.
|
||||
'''
|
||||
return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
|
||||
"\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64)))
|
||||
|
||||
|
||||
def pem_to_der(pem_filename=None, pem_content=None):
|
||||
'''
|
||||
Load PEM file, or use PEM file's content, and convert to DER.
|
||||
|
||||
If PEM contains multiple entities, the first entity will be used.
|
||||
'''
|
||||
certificate_lines = []
|
||||
if pem_content is not None:
|
||||
lines = pem_content.splitlines()
|
||||
elif pem_filename is not None:
|
||||
try:
|
||||
with open(pem_filename, "rt") as f:
|
||||
lines = list(f)
|
||||
except Exception as err:
|
||||
raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc())
|
||||
else:
|
||||
raise ModuleFailException('One of pem_filename and pem_content must be provided')
|
||||
header_line_count = 0
|
||||
for line in lines:
|
||||
if line.startswith('-----'):
|
||||
header_line_count += 1
|
||||
if header_line_count == 2:
|
||||
# If certificate file contains other certs appended
|
||||
# (like intermediate certificates), ignore these.
|
||||
break
|
||||
continue
|
||||
certificate_lines.append(line.strip())
|
||||
return base64.b64decode(''.join(certificate_lines))
|
||||
|
||||
|
||||
def process_links(info, callback):
|
||||
'''
|
||||
Process link header, calls callback for every link header with the URL and relation as options.
|
||||
'''
|
||||
if 'link' in info:
|
||||
link = info['link']
|
||||
for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
|
||||
callback(unquote(url), relation)
|
||||
@@ -55,7 +55,7 @@ from .cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
)
|
||||
|
||||
from .identify import (
|
||||
from .pem import (
|
||||
identify_private_key_format,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ __metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# 2.0, and the BSD License. See the LICENSE file at
|
||||
# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details.
|
||||
#
|
||||
# The Apache 2.0 license has been included as Apache-2.0.txt in this collection.
|
||||
#
|
||||
# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py
|
||||
#
|
||||
# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk)
|
||||
@@ -20,6 +22,10 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# WARNING: this function no longer works with cryptography 35.0.0 and newer!
|
||||
# It must **ONLY** be used in compatibility code for older
|
||||
# cryptography versions!
|
||||
|
||||
def obj2txt(openssl_lib, openssl_ffi, obj):
|
||||
# Set to 80 on the recommendation of
|
||||
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# In case the following data structure has any copyrightable content, note that it is licensed as follows:
|
||||
# Copyright (c) the OpenSSL contributors
|
||||
# Licensed under the Apache License 2.0
|
||||
# https://github.com/openssl/openssl/blob/master/LICENSE
|
||||
# https://github.com/openssl/openssl/blob/master/LICENSE.txt or Apache-2.0.txt
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
@@ -20,13 +20,13 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import OpenSSL # noqa
|
||||
from OpenSSL import crypto # noqa
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
@@ -65,11 +65,78 @@ try:
|
||||
x509.RFC822Name.__hash__ = simple_hash
|
||||
x509.UniformResourceIdentifier.__hash__ = simple_hash
|
||||
|
||||
# Test whether we have support for X25519, X448, Ed25519 and/or Ed448
|
||||
# Test whether we have support for DSA, EC, Ed25519, Ed448, RSA, X25519 and/or X448
|
||||
try:
|
||||
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/dsa/
|
||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||
CRYPTOGRAPHY_HAS_DSA = True
|
||||
try:
|
||||
# added later in 1.5
|
||||
cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey.sign
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN = True
|
||||
except AttributeError:
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN = False
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_DSA = False
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN = False
|
||||
try:
|
||||
# added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/
|
||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
||||
CRYPTOGRAPHY_HAS_ED25519 = True
|
||||
try:
|
||||
# added with the primitive in 2.6
|
||||
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.sign
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN = True
|
||||
except AttributeError:
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN = False
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN = False
|
||||
try:
|
||||
# added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/
|
||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
||||
CRYPTOGRAPHY_HAS_ED448 = True
|
||||
try:
|
||||
# added with the primitive in 2.6
|
||||
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.sign
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN = True
|
||||
except AttributeError:
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN = False
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_ED448 = False
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN = False
|
||||
try:
|
||||
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
CRYPTOGRAPHY_HAS_EC = True
|
||||
try:
|
||||
# added later in 1.5
|
||||
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey.sign
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN = True
|
||||
except AttributeError:
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN = False
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_EC = False
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN = False
|
||||
try:
|
||||
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
CRYPTOGRAPHY_HAS_RSA = True
|
||||
try:
|
||||
# added later in 1.4
|
||||
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.sign
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN = True
|
||||
except AttributeError:
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN = False
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_RSA = False
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN = False
|
||||
try:
|
||||
# added in 2.0 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/
|
||||
import cryptography.hazmat.primitives.asymmetric.x25519
|
||||
CRYPTOGRAPHY_HAS_X25519 = True
|
||||
try:
|
||||
# added later in 2.5
|
||||
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = True
|
||||
except AttributeError:
|
||||
@@ -78,29 +145,28 @@ try:
|
||||
CRYPTOGRAPHY_HAS_X25519 = False
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||
try:
|
||||
# added in 2.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x448/
|
||||
import cryptography.hazmat.primitives.asymmetric.x448
|
||||
CRYPTOGRAPHY_HAS_X448 = True
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_X448 = False
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
||||
CRYPTOGRAPHY_HAS_ED25519 = True
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
||||
CRYPTOGRAPHY_HAS_ED448 = True
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_ED448 = False
|
||||
|
||||
HAS_CRYPTOGRAPHY = True
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
CRYPTOGRAPHY_HAS_EC = False
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN = False
|
||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN = False
|
||||
CRYPTOGRAPHY_HAS_ED448 = False
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN = False
|
||||
CRYPTOGRAPHY_HAS_DSA = False
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN = False
|
||||
CRYPTOGRAPHY_HAS_RSA = False
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN = False
|
||||
CRYPTOGRAPHY_HAS_X25519 = False
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||
CRYPTOGRAPHY_HAS_X448 = False
|
||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||
CRYPTOGRAPHY_HAS_ED448 = False
|
||||
HAS_CRYPTOGRAPHY = False
|
||||
|
||||
|
||||
|
||||
@@ -23,21 +23,70 @@ import base64
|
||||
import binascii
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.common.text.converters import to_text, to_bytes
|
||||
from ._asn1 import serialize_asn1_string_as_der
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# This is a separate try/except since this is only present in cryptography 2.5 or newer
|
||||
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||
load_key_and_certificates as _load_key_and_certificates,
|
||||
)
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
_load_key_and_certificates = None
|
||||
|
||||
try:
|
||||
# This is a separate try/except since this is only present in cryptography 36.0.0 or newer
|
||||
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||
load_pkcs12 as _load_pkcs12,
|
||||
)
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
_load_pkcs12 = None
|
||||
|
||||
from .basic import (
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN,
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN,
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
@@ -51,67 +100,126 @@ from ._objects import (
|
||||
from ._obj2txt import obj2txt
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_cert(cert):
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
result = dict()
|
||||
backend = cert._backend
|
||||
x509_obj = cert._x509
|
||||
DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_cert(cert):
|
||||
result = dict()
|
||||
try:
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
backend = default_backend()
|
||||
try:
|
||||
# For certain old versions of cryptography, backend is a MultiBackend object,
|
||||
# which has no _lib attribute. In that case, revert to the old approach.
|
||||
backend._lib
|
||||
except AttributeError:
|
||||
backend = cert._backend
|
||||
|
||||
x509_obj = cert._x509
|
||||
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
|
||||
# not allow to get the raw value of an extension, so we have to use this ugly hack:
|
||||
exts = list(cert.extensions)
|
||||
|
||||
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
|
||||
ext = backend._lib.X509_get_ext(x509_obj, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
try:
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
except AttributeError:
|
||||
oid = exts[i].oid.dotted_string
|
||||
result[oid] = entry
|
||||
|
||||
except Exception:
|
||||
# In case the above method breaks, we likely have cryptography 36.0.0 or newer.
|
||||
# Use it's public_bytes() feature in that case. We will later switch this around
|
||||
# so that this code will be the default, but for now this will act as a fallback
|
||||
# since it will re-serialize de-serialized data, which can be different (if the
|
||||
# original data was not canonicalized) from what was contained in the certificate.
|
||||
for ext in cert.extensions:
|
||||
result[ext.oid.dotted_string] = dict(
|
||||
critical=ext.critical,
|
||||
value=base64.b64encode(ext.value.public_bytes()),
|
||||
)
|
||||
|
||||
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
|
||||
ext = backend._lib.X509_get_ext(x509_obj, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_csr(csr):
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
result = dict()
|
||||
backend = csr._backend
|
||||
try:
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
backend = default_backend()
|
||||
try:
|
||||
# For certain old versions of cryptography, backend is a MultiBackend object,
|
||||
# which has no _lib attribute. In that case, revert to the old approach.
|
||||
backend._lib
|
||||
except AttributeError:
|
||||
backend = csr._backend
|
||||
|
||||
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||
extensions = backend._ffi.gc(
|
||||
extensions,
|
||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||
ext,
|
||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||
extensions = backend._ffi.gc(
|
||||
extensions,
|
||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||
ext,
|
||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
result[oid] = entry
|
||||
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
|
||||
# not allow to get the raw value of an extension, so we have to use this ugly hack:
|
||||
exts = list(csr.extensions)
|
||||
|
||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
try:
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
except AttributeError:
|
||||
oid = exts[i].oid.dotted_string
|
||||
result[oid] = entry
|
||||
|
||||
except Exception:
|
||||
# In case the above method breaks, we likely have cryptography 36.0.0 or newer.
|
||||
# Use it's public_bytes() feature in that case. We will later switch this around
|
||||
# so that this code will be the default, but for now this will act as a fallback
|
||||
# since it will re-serialize de-serialized data, which can be different (if the
|
||||
# original data was not canonicalized) from what was contained in the CSR.
|
||||
for ext in csr.extensions:
|
||||
result[ext.oid.dotted_string] = dict(
|
||||
critical=ext.critical,
|
||||
value=base64.b64encode(ext.value.public_bytes()),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_name_to_oid(name):
|
||||
dotted = OID_LOOKUP.get(name)
|
||||
if dotted is None:
|
||||
if DOTTED_OID.match(name):
|
||||
return x509.oid.ObjectIdentifier(name)
|
||||
raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name))
|
||||
return x509.oid.ObjectIdentifier(dotted)
|
||||
|
||||
@@ -119,7 +227,12 @@ def cryptography_name_to_oid(name):
|
||||
def cryptography_oid_to_name(oid, short=False):
|
||||
dotted_string = oid.dotted_string
|
||||
names = OID_MAP.get(dotted_string)
|
||||
name = names[0] if names else oid._name
|
||||
if names:
|
||||
name = names[0]
|
||||
else:
|
||||
name = oid._name
|
||||
if name == 'Unknown OID':
|
||||
name = dotted_string
|
||||
if short:
|
||||
return NORMALIZE_NAMES_SHORT.get(name, name)
|
||||
else:
|
||||
@@ -142,6 +255,36 @@ def _parse_hex(bytesstr):
|
||||
return data
|
||||
|
||||
|
||||
DN_COMPONENT_START_RE = re.compile(r'^ *([a-zA-z0-9]+) *= *')
|
||||
|
||||
|
||||
def _parse_dn_component(name, sep=',', sep_str='\\', decode_remainder=True):
|
||||
m = DN_COMPONENT_START_RE.match(name)
|
||||
if not m:
|
||||
raise OpenSSLObjectError('cannot start part in "{0}"'.format(name))
|
||||
oid = cryptography_name_to_oid(m.group(1))
|
||||
idx = len(m.group(0))
|
||||
decoded_name = []
|
||||
if decode_remainder:
|
||||
length = len(name)
|
||||
while idx < length:
|
||||
i = idx
|
||||
while i < length and name[i] not in sep_str:
|
||||
i += 1
|
||||
if i > idx:
|
||||
decoded_name.append(name[idx:i])
|
||||
idx = i
|
||||
while idx + 1 < length and name[idx] == '\\':
|
||||
decoded_name.append(name[idx + 1])
|
||||
idx += 2
|
||||
if idx < length and name[idx] == sep:
|
||||
break
|
||||
else:
|
||||
decoded_name.append(name[idx:])
|
||||
idx = len(name)
|
||||
return x509.NameAttribute(oid, ''.join(decoded_name)), name[idx:]
|
||||
|
||||
|
||||
def _parse_dn(name):
|
||||
'''
|
||||
Parse a Distinguished Name.
|
||||
@@ -156,29 +299,12 @@ def _parse_dn(name):
|
||||
name = name[1:]
|
||||
sep_str = sep + '\\'
|
||||
result = []
|
||||
start_re = re.compile(r'^ *([a-zA-z0-9]+) *= *')
|
||||
while name:
|
||||
m = start_re.match(name)
|
||||
if not m:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": cannot start part in "{1}"'.format(original_name, name))
|
||||
oid = cryptography_name_to_oid(m.group(1))
|
||||
idx = len(m.group(0))
|
||||
decoded_name = []
|
||||
length = len(name)
|
||||
while idx < length:
|
||||
i = idx
|
||||
while i < length and name[i] not in sep_str:
|
||||
i += 1
|
||||
if i > idx:
|
||||
decoded_name.append(name[idx:i])
|
||||
idx = i
|
||||
while idx + 1 < length and name[idx] == '\\':
|
||||
decoded_name.append(name[idx + 1])
|
||||
idx += 2
|
||||
if idx < length and name[idx] == sep:
|
||||
break
|
||||
result.append(x509.NameAttribute(oid, ''.join(decoded_name)))
|
||||
name = name[idx:]
|
||||
try:
|
||||
attribute, name = _parse_dn_component(name, sep=sep, sep_str=sep_str)
|
||||
except OpenSSLObjectError as e:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": {1}'.format(original_name, e))
|
||||
result.append(attribute)
|
||||
if name:
|
||||
if name[0] != sep or len(name) < 2:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": unexpected end of string'.format(original_name))
|
||||
@@ -186,16 +312,29 @@ def _parse_dn(name):
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_get_name(name):
|
||||
def cryptography_parse_relative_distinguished_name(rdn):
|
||||
names = []
|
||||
for part in rdn:
|
||||
try:
|
||||
names.append(_parse_dn_component(to_text(part), decode_remainder=False)[0])
|
||||
except OpenSSLObjectError as e:
|
||||
raise OpenSSLObjectError('Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
|
||||
return cryptography.x509.RelativeDistinguishedName(names)
|
||||
|
||||
|
||||
def cryptography_get_name(name, what='Subject Alternative Name'):
|
||||
'''
|
||||
Given a name string, returns a cryptography x509.Name object.
|
||||
Given a name string, returns a cryptography x509.GeneralName object.
|
||||
Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
|
||||
'''
|
||||
try:
|
||||
if name.startswith('DNS:'):
|
||||
return x509.DNSName(to_text(name[4:]))
|
||||
if name.startswith('IP:'):
|
||||
return x509.IPAddress(ipaddress.ip_address(to_text(name[3:])))
|
||||
address = to_text(name[3:])
|
||||
if '/' in address:
|
||||
return x509.IPAddress(ipaddress.ip_network(address))
|
||||
return x509.IPAddress(ipaddress.ip_address(address))
|
||||
if name.startswith('email:'):
|
||||
return x509.RFC822Name(to_text(name[6:]))
|
||||
if name.startswith('URI:'):
|
||||
@@ -203,7 +342,7 @@ def cryptography_get_name(name):
|
||||
if name.startswith('RID:'):
|
||||
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
|
||||
if not m:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}"'.format(name))
|
||||
raise OpenSSLObjectError('Cannot parse {what} "{name}"'.format(name=name, what=what))
|
||||
return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1)))
|
||||
if name.startswith('otherName:'):
|
||||
# otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with.
|
||||
@@ -215,9 +354,9 @@ def cryptography_get_name(name):
|
||||
# defailts on the format expected.
|
||||
name = to_text(name[10:], errors='surrogate_or_strict')
|
||||
if ';' not in name:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name otherName "{0}", must be in the '
|
||||
raise OpenSSLObjectError('Cannot parse {what} otherName "{name}", must be in the '
|
||||
'format "otherName:<OID>;<ASN.1 OpenSSL Encoded String>" or '
|
||||
'"otherName:<OID>;<hex string>"'.format(name))
|
||||
'"otherName:<OID>;<hex string>"'.format(name=name, what=what))
|
||||
|
||||
oid, value = name.split(';', 1)
|
||||
b_value = serialize_asn1_string_as_der(value)
|
||||
@@ -225,46 +364,48 @@ def cryptography_get_name(name):
|
||||
if name.startswith('dirName:'):
|
||||
return x509.DirectoryName(x509.Name(_parse_dn(to_text(name[8:]))))
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}": {1}'.format(name, e))
|
||||
raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e))
|
||||
if ':' not in name:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name))
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name))
|
||||
raise OpenSSLObjectError('Cannot parse {what} "{name}" (forgot "DNS:" prefix?)'.format(name=name, what=what))
|
||||
raise OpenSSLObjectError('Cannot parse {what} "{name}" (potentially unsupported by cryptography backend)'.format(name=name, what=what))
|
||||
|
||||
|
||||
def _dn_escape_value(value):
|
||||
'''
|
||||
Escape Distinguished Name's attribute value.
|
||||
'''
|
||||
value = value.replace('\\', '\\\\')
|
||||
for ch in [',', '#', '+', '<', '>', ';', '"', '=', '/']:
|
||||
value = value.replace(ch, '\\%s' % ch)
|
||||
if value.startswith(' '):
|
||||
value = r'\ ' + value[1:]
|
||||
value = value.replace(u'\\', u'\\\\')
|
||||
for ch in [u',', u'#', u'+', u'<', u'>', u';', u'"', u'=', u'/']:
|
||||
value = value.replace(ch, u'\\%s' % ch)
|
||||
if value.startswith(u' '):
|
||||
value = u'\\ ' + value[1:]
|
||||
return value
|
||||
|
||||
|
||||
def cryptography_decode_name(name):
|
||||
'''
|
||||
Given a cryptography x509.Name object, returns a string.
|
||||
Given a cryptography x509.GeneralName object, returns a string.
|
||||
Raises an OpenSSLObjectError if the name is not supported.
|
||||
'''
|
||||
if isinstance(name, x509.DNSName):
|
||||
return 'DNS:{0}'.format(name.value)
|
||||
return u'DNS:{0}'.format(name.value)
|
||||
if isinstance(name, x509.IPAddress):
|
||||
return 'IP:{0}'.format(name.value.compressed)
|
||||
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
||||
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
||||
return u'IP:{0}'.format(name.value.compressed)
|
||||
if isinstance(name, x509.RFC822Name):
|
||||
return 'email:{0}'.format(name.value)
|
||||
return u'email:{0}'.format(name.value)
|
||||
if isinstance(name, x509.UniformResourceIdentifier):
|
||||
return 'URI:{0}'.format(name.value)
|
||||
return u'URI:{0}'.format(name.value)
|
||||
if isinstance(name, x509.DirectoryName):
|
||||
return 'dirName:' + ''.join([
|
||||
'/{0}={1}'.format(cryptography_oid_to_name(attribute.oid, short=True), _dn_escape_value(attribute.value))
|
||||
return u'dirName:' + u''.join([
|
||||
u'/{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
|
||||
for attribute in name.value
|
||||
])
|
||||
if isinstance(name, x509.RegisteredID):
|
||||
return 'RID:{0}'.format(name.value.dotted_string)
|
||||
return u'RID:{0}'.format(name.value.dotted_string)
|
||||
if isinstance(name, x509.OtherName):
|
||||
return 'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
return u'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
|
||||
|
||||
|
||||
@@ -390,3 +531,116 @@ def cryptography_serial_number_of_cert(cert):
|
||||
except AttributeError:
|
||||
# The property was called "serial" before cryptography 1.4
|
||||
return cert.serial
|
||||
|
||||
|
||||
def parse_pkcs12(pkcs12_bytes, passphrase=None):
|
||||
'''Returns a tuple (private_key, certificate, additional_certificates, friendly_name).
|
||||
'''
|
||||
if _load_pkcs12 is None and _load_key_and_certificates is None:
|
||||
raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version')
|
||||
|
||||
if passphrase is not None:
|
||||
passphrase = to_bytes(passphrase)
|
||||
|
||||
# Main code for cryptography 36.0.0 and forward
|
||||
if _load_pkcs12 is not None:
|
||||
return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
|
||||
|
||||
if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'):
|
||||
return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
|
||||
|
||||
return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
|
||||
|
||||
|
||||
def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
|
||||
# Requires cryptography 36.0.0 or newer
|
||||
pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase)
|
||||
additional_certificates = [cert.certificate for cert in pkcs12.additional_certs]
|
||||
private_key = pkcs12.key
|
||||
certificate = None
|
||||
friendly_name = None
|
||||
if pkcs12.cert:
|
||||
certificate = pkcs12.cert.certificate
|
||||
friendly_name = pkcs12.cert.friendly_name
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
|
||||
# Backwards compatibility code for cryptography 35.x
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||
|
||||
friendly_name = None
|
||||
if certificate:
|
||||
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||
backend = default_backend()
|
||||
|
||||
# This code basically does what load_key_and_certificates() does, but without error-checking.
|
||||
# Since load_key_and_certificates succeeded, it should not fail.
|
||||
pkcs12 = backend._ffi.gc(
|
||||
backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL),
|
||||
backend._lib.PKCS12_free)
|
||||
certificate_x509_ptr = backend._ffi.new("X509 **")
|
||||
with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer:
|
||||
backend._lib.PKCS12_parse(
|
||||
pkcs12,
|
||||
passphrase_buffer,
|
||||
backend._ffi.new("EVP_PKEY **"),
|
||||
certificate_x509_ptr,
|
||||
backend._ffi.new("Cryptography_STACK_OF_X509 **"))
|
||||
if certificate_x509_ptr[0] != backend._ffi.NULL:
|
||||
maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL)
|
||||
if maybe_name != backend._ffi.NULL:
|
||||
friendly_name = backend._ffi.string(maybe_name)
|
||||
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
|
||||
# Backwards compatibility code for cryptography < 35.0.0
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||
|
||||
friendly_name = None
|
||||
if certificate:
|
||||
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||
backend = certificate._backend
|
||||
maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL)
|
||||
if maybe_name != backend._ffi.NULL:
|
||||
friendly_name = backend._ffi.string(maybe_name)
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key):
|
||||
'''
|
||||
Check whether the given signature of the given data was signed by the given public key object.
|
||||
'''
|
||||
try:
|
||||
if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
||||
signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm)
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
||||
signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm))
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
|
||||
signer_public_key.verify(signature, data, hash_algorithm)
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
|
||||
signer_public_key.verify(signature, data)
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
|
||||
signer_public_key.verify(signature, data)
|
||||
return True
|
||||
raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key)))
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
def cryptography_verify_certificate_signature(certificate, signer_public_key):
|
||||
'''
|
||||
Check whether the given X509 certificate object was signed by the given public key object.
|
||||
'''
|
||||
return cryptography_verify_signature(
|
||||
certificate.signature,
|
||||
certificate.tbs_certificate_bytes,
|
||||
certificate.signature_hash_algorithm,
|
||||
signer_public_key
|
||||
)
|
||||
|
||||
@@ -53,9 +53,18 @@ def quick_is_not_prime(n):
|
||||
that we couldn't 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)
|
||||
|
||||
416
plugins/module_utils/crypto/module_backends/certificate.py
Normal file
416
plugins/module_utils/crypto/module_backends/certificate.py
Normal file
@@ -0,0 +1,416 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import 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.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_privatekey,
|
||||
load_certificate,
|
||||
load_certificate_request,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
|
||||
get_certificate_info,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
CRYPTOGRAPHY_VERSION = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
class CertificateError(OpenSSLObjectError):
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CertificateBackend(object):
|
||||
def __init__(self, module, backend):
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
|
||||
self.force = module.params['force']
|
||||
self.privatekey_path = module.params['privatekey_path']
|
||||
self.privatekey_content = module.params['privatekey_content']
|
||||
if self.privatekey_content is not None:
|
||||
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||
self.csr_path = module.params['csr_path']
|
||||
self.csr_content = module.params['csr_content']
|
||||
if self.csr_content is not None:
|
||||
self.csr_content = self.csr_content.encode('utf-8')
|
||||
|
||||
# The following are default values which make sure check() works as
|
||||
# before if providers do not explicitly change these properties.
|
||||
self.create_subject_key_identifier = 'never_create'
|
||||
self.create_authority_key_identifier = False
|
||||
|
||||
self.privatekey = None
|
||||
self.csr = None
|
||||
self.cert = None
|
||||
self.existing_certificate = None
|
||||
self.existing_certificate_bytes = None
|
||||
|
||||
self.check_csr_subject = True
|
||||
self.check_csr_extensions = True
|
||||
|
||||
self.diff_before = self._get_info(None)
|
||||
self.diff_after = self._get_info(None)
|
||||
|
||||
def _get_info(self, data):
|
||||
if data is None:
|
||||
return dict()
|
||||
try:
|
||||
result = get_certificate_info(self.module, self.backend, data, prefer_one_fingerprint=True)
|
||||
result['can_parse_certificate'] = True
|
||||
return result
|
||||
except Exception as exc:
|
||||
return dict(can_parse_certificate=False)
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
pass
|
||||
|
||||
def set_existing(self, certificate_bytes):
|
||||
"""Set existing certificate bytes. None indicates that the key does not exist."""
|
||||
self.existing_certificate_bytes = certificate_bytes
|
||||
self.diff_after = self.diff_before = self._get_info(self.existing_certificate_bytes)
|
||||
|
||||
def has_existing(self):
|
||||
"""Query whether an existing certificate is/has been there."""
|
||||
return self.existing_certificate_bytes is not None
|
||||
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Load the provided private key into self.privatekey."""
|
||||
if self.privatekey is not None:
|
||||
return
|
||||
if self.privatekey_path is None and self.privatekey_content is None:
|
||||
return
|
||||
try:
|
||||
self.privatekey = load_privatekey(
|
||||
path=self.privatekey_path,
|
||||
content=self.privatekey_content,
|
||||
passphrase=self.privatekey_passphrase,
|
||||
backend=self.backend,
|
||||
)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise CertificateError(exc)
|
||||
|
||||
def _ensure_csr_loaded(self):
|
||||
"""Load the CSR into self.csr."""
|
||||
if self.csr is not None:
|
||||
return
|
||||
if self.csr_path is None and self.csr_content is None:
|
||||
return
|
||||
self.csr = load_certificate_request(
|
||||
path=self.csr_path,
|
||||
content=self.csr_content,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
def _ensure_existing_certificate_loaded(self):
|
||||
"""Load the existing certificate into self.existing_certificate."""
|
||||
if self.existing_certificate is not None:
|
||||
return
|
||||
if self.existing_certificate_bytes is None:
|
||||
return
|
||||
self.existing_certificate = load_certificate(
|
||||
path=None,
|
||||
content=self.existing_certificate_bytes,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
def _check_privatekey(self):
|
||||
"""Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated."""
|
||||
if self.backend == 'pyopenssl':
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
|
||||
ctx.use_privatekey(self.privatekey)
|
||||
ctx.use_certificate(self.existing_certificate)
|
||||
try:
|
||||
ctx.check_privatekey()
|
||||
return True
|
||||
except OpenSSL.SSL.Error:
|
||||
return False
|
||||
elif self.backend == 'cryptography':
|
||||
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
|
||||
|
||||
def _check_csr(self):
|
||||
"""Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated."""
|
||||
if self.backend == 'pyopenssl':
|
||||
# Verify that CSR is signed by certificate's private key
|
||||
try:
|
||||
self.csr.verify(self.existing_certificate.get_pubkey())
|
||||
except OpenSSL.crypto.Error:
|
||||
return False
|
||||
# Check subject
|
||||
if self.check_csr_subject and self.csr.get_subject() != self.existing_certificate.get_subject():
|
||||
return False
|
||||
# Check extensions
|
||||
if not self.check_csr_extensions:
|
||||
return True
|
||||
csr_extensions = self.csr.get_extensions()
|
||||
cert_extension_count = self.existing_certificate.get_extension_count()
|
||||
if len(csr_extensions) != cert_extension_count:
|
||||
return False
|
||||
for extension_number in range(0, cert_extension_count):
|
||||
cert_extension = self.existing_certificate.get_extension(extension_number)
|
||||
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
|
||||
if cert_extension.get_data() != list(csr_extension)[0].get_data():
|
||||
return False
|
||||
return True
|
||||
elif self.backend == 'cryptography':
|
||||
# Verify that CSR is signed by certificate's private key
|
||||
if not self.csr.is_signature_valid:
|
||||
return False
|
||||
if not cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key()):
|
||||
return False
|
||||
# Check subject
|
||||
if self.check_csr_subject and self.csr.subject != self.existing_certificate.subject:
|
||||
return False
|
||||
# Check extensions
|
||||
if not self.check_csr_extensions:
|
||||
return True
|
||||
cert_exts = list(self.existing_certificate.extensions)
|
||||
csr_exts = list(self.csr.extensions)
|
||||
if self.create_subject_key_identifier != 'never_create':
|
||||
# Filter out SubjectKeyIdentifier extension before comparison
|
||||
cert_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), cert_exts))
|
||||
csr_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), csr_exts))
|
||||
if self.create_authority_key_identifier:
|
||||
# Filter out AuthorityKeyIdentifier extension before comparison
|
||||
cert_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), cert_exts))
|
||||
csr_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), csr_exts))
|
||||
if len(cert_exts) != len(csr_exts):
|
||||
return False
|
||||
for cert_ext in cert_exts:
|
||||
try:
|
||||
csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid)
|
||||
if cert_ext != csr_ext:
|
||||
return False
|
||||
except cryptography.x509.ExtensionNotFound as dummy:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_subject_key_identifier(self):
|
||||
"""Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated."""
|
||||
if self.backend != 'cryptography':
|
||||
# We do not support SKI with pyOpenSSL backend
|
||||
return True
|
||||
|
||||
# Get hold of certificate's SKI
|
||||
try:
|
||||
ext = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
except cryptography.x509.ExtensionNotFound as dummy:
|
||||
return False
|
||||
# Get hold of CSR's SKI for 'create_if_not_provided'
|
||||
csr_ext = None
|
||||
if self.create_subject_key_identifier == 'create_if_not_provided':
|
||||
try:
|
||||
csr_ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
except cryptography.x509.ExtensionNotFound as dummy:
|
||||
pass
|
||||
if csr_ext is None:
|
||||
# If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI
|
||||
if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.existing_certificate.public_key()).digest:
|
||||
return False
|
||||
else:
|
||||
# If CSR had SKI and we didn't ignore it ('create_if_not_provided'), compare SKIs
|
||||
if ext.value.digest != csr_ext.value.digest:
|
||||
return False
|
||||
return True
|
||||
|
||||
def needs_regeneration(self):
|
||||
"""Check whether a regeneration is necessary."""
|
||||
if self.force or self.existing_certificate_bytes is None:
|
||||
return True
|
||||
|
||||
try:
|
||||
self._ensure_existing_certificate_loaded()
|
||||
except Exception as dummy:
|
||||
return True
|
||||
|
||||
# Check whether private key matches
|
||||
self._ensure_private_key_loaded()
|
||||
if self.privatekey is not None and not self._check_privatekey():
|
||||
return True
|
||||
|
||||
# Check whether CSR matches
|
||||
self._ensure_csr_loaded()
|
||||
if self.csr is not None and not self._check_csr():
|
||||
return True
|
||||
|
||||
# Check SubjectKeyIdentifier
|
||||
if self.create_subject_key_identifier != 'never_create' and not self._check_subject_key_identifier():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = {
|
||||
'privatekey': self.privatekey_path,
|
||||
'csr': self.csr_path
|
||||
}
|
||||
# Get hold of certificate bytes
|
||||
certificate_bytes = self.existing_certificate_bytes
|
||||
if self.cert is not None:
|
||||
certificate_bytes = self.get_certificate_data()
|
||||
self.diff_after = self._get_info(certificate_bytes)
|
||||
if include_certificate:
|
||||
# Store result
|
||||
result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None
|
||||
|
||||
result['diff'] = dict(
|
||||
before=self.diff_before,
|
||||
after=self.diff_after,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CertificateProvider(object):
|
||||
@abc.abstractmethod
|
||||
def validate_module_args(self, module):
|
||||
"""Check module arguments"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def needs_version_two_certs(self, module):
|
||||
"""Whether the provider needs to create a version 2 certificate."""
|
||||
|
||||
def needs_pyopenssl_get_extensions(self, module):
|
||||
"""Whether the provider needs to use get_extensions() with pyOpenSSL."""
|
||||
return True
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_backend(self, module, backend):
|
||||
"""Create an implementation for a backend.
|
||||
|
||||
Return value must be instance of CertificateBackend.
|
||||
"""
|
||||
|
||||
|
||||
def select_backend(module, backend, provider):
|
||||
"""
|
||||
:type module: AnsibleModule
|
||||
:type backend: str
|
||||
:type provider: CertificateProvider
|
||||
"""
|
||||
provider.validate_module_args(module)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detect what backend we can use
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# If cryptography is available we'll use it
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
if provider.needs_version_two_certs(module):
|
||||
module.warn('crypto backend forced to pyopenssl. The cryptography library does not support v2 certificates')
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Fail if no backend has been found
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
if provider.needs_pyopenssl_get_extensions(module):
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
||||
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
if provider.needs_version_two_certs(module):
|
||||
module.fail_json(msg='The cryptography backend does not support v2 certificates, '
|
||||
'use select_crypto_backend=pyopenssl for v2 certificates')
|
||||
|
||||
return provider.create_backend(module, backend)
|
||||
|
||||
|
||||
def get_certificate_argument_spec():
|
||||
return ArgumentSpec(
|
||||
argument_spec=dict(
|
||||
provider=dict(type='str', choices=[]), # choices will be filled by add_XXX_provider_to_argument_spec() in certificate_xxx.py
|
||||
force=dict(type='bool', default=False,),
|
||||
csr_path=dict(type='path'),
|
||||
csr_content=dict(type='str'),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
|
||||
# General properties of a certificate
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
),
|
||||
mutually_exclusive=[
|
||||
['csr_path', 'csr_content'],
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
],
|
||||
)
|
||||
119
plugins/module_utils/crypto/module_backends/certificate_acme.py
Normal file
119
plugins/module_utils/crypto/module_backends/certificate_acme.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
|
||||
class AcmeCertificateBackend(CertificateBackend):
|
||||
def __init__(self, module, backend):
|
||||
super(AcmeCertificateBackend, self).__init__(module, backend)
|
||||
self.accountkey_path = module.params['acme_accountkey_path']
|
||||
self.challenge_path = module.params['acme_challenge_path']
|
||||
self.use_chain = module.params['acme_chain']
|
||||
self.acme_directory = module.params['acme_directory']
|
||||
|
||||
if self.csr_content is None and self.csr_path is None:
|
||||
raise CertificateError(
|
||||
'csr_path or csr_content is required for ownca provider'
|
||||
)
|
||||
if self.csr_content is None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file %s does not exist' % self.csr_path
|
||||
)
|
||||
|
||||
if not os.path.exists(self.accountkey_path):
|
||||
raise CertificateError(
|
||||
'The account key %s does not exist' % self.accountkey_path
|
||||
)
|
||||
|
||||
if not os.path.exists(self.challenge_path):
|
||||
raise CertificateError(
|
||||
'The challenge path %s does not exist' % self.challenge_path
|
||||
)
|
||||
|
||||
self.acme_tiny_path = self.module.get_bin_path('acme-tiny', required=True)
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
|
||||
command = [self.acme_tiny_path]
|
||||
if self.use_chain:
|
||||
command.append('--chain')
|
||||
command.extend(['--account-key', self.accountkey_path])
|
||||
if self.csr_content is not None:
|
||||
# We need to temporarily write the CSR to disk
|
||||
fd, tmpsrc = tempfile.mkstemp()
|
||||
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
f.write(self.csr_content)
|
||||
except Exception as err:
|
||||
try:
|
||||
f.close()
|
||||
except Exception as dummy:
|
||||
pass
|
||||
self.module.fail_json(
|
||||
msg="failed to create temporary CSR file: %s" % to_native(err),
|
||||
exception=traceback.format_exc()
|
||||
)
|
||||
f.close()
|
||||
command.extend(['--csr', tmpsrc])
|
||||
else:
|
||||
command.extend(['--csr', self.csr_path])
|
||||
command.extend(['--acme-dir', self.challenge_path])
|
||||
command.extend(['--directory-url', self.acme_directory])
|
||||
|
||||
try:
|
||||
self.cert = to_bytes(self.module.run_command(command, check_rc=True)[1])
|
||||
except OSError as exc:
|
||||
raise CertificateError(exc)
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(AcmeCertificateBackend, self).dump(include_certificate)
|
||||
result['accountkey'] = self.accountkey_path
|
||||
return result
|
||||
|
||||
|
||||
class AcmeCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
if module.params['acme_accountkey_path'] is None:
|
||||
module.fail_json(msg='The acme_accountkey_path option must be specified for the acme provider.')
|
||||
if module.params['acme_challenge_path'] is None:
|
||||
module.fail_json(msg='The acme_challenge_path option must be specified for the acme provider.')
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return False
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
return AcmeCertificateBackend(module, backend)
|
||||
|
||||
|
||||
def add_acme_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('acme')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
acme_accountkey_path=dict(type='path'),
|
||||
acme_challenge_path=dict(type='path'),
|
||||
acme_chain=dict(type='bool', default=False),
|
||||
acme_directory=dict(type='str', default="https://acme-v02.api.letsencrypt.org/directory"),
|
||||
))
|
||||
@@ -0,0 +1,671 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import datetime
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
parse_name_field,
|
||||
get_relative_time_option,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
cryptography_get_name,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_parse_key_usage_params,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.x509 import NameAttribute, Name
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def compare_sets(subset, superset, equality=False):
|
||||
if equality:
|
||||
return set(subset) == set(superset)
|
||||
else:
|
||||
return all(x in superset for x in subset)
|
||||
|
||||
|
||||
def compare_dicts(subset, superset, equality=False):
|
||||
if equality:
|
||||
return subset == superset
|
||||
else:
|
||||
return all(superset.get(x) == v for x, v in subset.items())
|
||||
|
||||
|
||||
NO_EXTENSION = 'no extension'
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackend(CertificateBackend):
|
||||
def __init__(self, module, backend):
|
||||
super(AssertOnlyCertificateBackend, self).__init__(module, backend)
|
||||
|
||||
self.signature_algorithms = module.params['signature_algorithms']
|
||||
if module.params['subject']:
|
||||
self.subject = parse_name_field(module.params['subject'])
|
||||
else:
|
||||
self.subject = []
|
||||
self.subject_strict = module.params['subject_strict']
|
||||
if module.params['issuer']:
|
||||
self.issuer = parse_name_field(module.params['issuer'])
|
||||
else:
|
||||
self.issuer = []
|
||||
self.issuer_strict = module.params['issuer_strict']
|
||||
self.has_expired = module.params['has_expired']
|
||||
self.version = module.params['version']
|
||||
self.key_usage = module.params['key_usage']
|
||||
self.key_usage_strict = module.params['key_usage_strict']
|
||||
self.extended_key_usage = module.params['extended_key_usage']
|
||||
self.extended_key_usage_strict = module.params['extended_key_usage_strict']
|
||||
self.subject_alt_name = module.params['subject_alt_name']
|
||||
self.subject_alt_name_strict = module.params['subject_alt_name_strict']
|
||||
self.not_before = module.params['not_before']
|
||||
self.not_after = module.params['not_after']
|
||||
self.valid_at = module.params['valid_at']
|
||||
self.invalid_at = module.params['invalid_at']
|
||||
self.valid_in = module.params['valid_in']
|
||||
if self.valid_in and not self.valid_in.startswith("+") and not self.valid_in.startswith("-"):
|
||||
try:
|
||||
int(self.valid_in)
|
||||
except ValueError:
|
||||
module.fail_json(msg='The supplied value for "valid_in" (%s) is not an integer or a valid timespec' % self.valid_in)
|
||||
self.valid_in = "+" + self.valid_in + "s"
|
||||
|
||||
# Load objects
|
||||
self._ensure_private_key_loaded()
|
||||
self._ensure_csr_loaded()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_privatekey(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_signature(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_subject(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_signature_algorithms(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_subject(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_issuer(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_has_expired(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_version(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_not_before(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_not_after(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_valid_at(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_invalid_at(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_valid_in(self):
|
||||
pass
|
||||
|
||||
def assertonly(self):
|
||||
messages = []
|
||||
if self.privatekey_path is not None or self.privatekey_content is not None:
|
||||
if not self._validate_privatekey():
|
||||
messages.append(
|
||||
'Certificate and private key %s do not match' %
|
||||
(self.privatekey_path or '(provided in module options)')
|
||||
)
|
||||
|
||||
if self.csr_path is not None or self.csr_content is not None:
|
||||
if not self._validate_csr_signature():
|
||||
messages.append(
|
||||
'Certificate and CSR %s do not match: private key mismatch' %
|
||||
(self.csr_path or '(provided in module options)')
|
||||
)
|
||||
if not self._validate_csr_subject():
|
||||
messages.append(
|
||||
'Certificate and CSR %s do not match: subject mismatch' %
|
||||
(self.csr_path or '(provided in module options)')
|
||||
)
|
||||
if not self._validate_csr_extensions():
|
||||
messages.append(
|
||||
'Certificate and CSR %s do not match: extensions mismatch' %
|
||||
(self.csr_path or '(provided in module options)')
|
||||
)
|
||||
|
||||
if self.signature_algorithms is not None:
|
||||
wrong_alg = self._validate_signature_algorithms()
|
||||
if wrong_alg:
|
||||
messages.append(
|
||||
'Invalid signature algorithm (got %s, expected one of %s)' %
|
||||
(wrong_alg, self.signature_algorithms)
|
||||
)
|
||||
|
||||
if self.subject is not None:
|
||||
failure = self._validate_subject()
|
||||
if failure:
|
||||
dummy, cert_subject = failure
|
||||
messages.append(
|
||||
'Invalid subject component (got %s, expected all of %s to be present)' %
|
||||
(cert_subject, self.subject)
|
||||
)
|
||||
|
||||
if self.issuer is not None:
|
||||
failure = self._validate_issuer()
|
||||
if failure:
|
||||
dummy, cert_issuer = failure
|
||||
messages.append(
|
||||
'Invalid issuer component (got %s, expected all of %s to be present)' % (cert_issuer, self.issuer)
|
||||
)
|
||||
|
||||
if self.has_expired is not None:
|
||||
cert_expired = self._validate_has_expired()
|
||||
if cert_expired != self.has_expired:
|
||||
messages.append(
|
||||
'Certificate expiration check failed (certificate expiration is %s, expected %s)' %
|
||||
(cert_expired, self.has_expired)
|
||||
)
|
||||
|
||||
if self.version is not None:
|
||||
cert_version = self._validate_version()
|
||||
if cert_version != self.version:
|
||||
messages.append(
|
||||
'Invalid certificate version number (got %s, expected %s)' %
|
||||
(cert_version, self.version)
|
||||
)
|
||||
|
||||
if self.key_usage is not None:
|
||||
failure = self._validate_key_usage()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no keyUsage extension')
|
||||
elif failure:
|
||||
dummy, cert_key_usage = failure
|
||||
messages.append(
|
||||
'Invalid keyUsage components (got %s, expected all of %s to be present)' %
|
||||
(cert_key_usage, self.key_usage)
|
||||
)
|
||||
|
||||
if self.extended_key_usage is not None:
|
||||
failure = self._validate_extended_key_usage()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no extendedKeyUsage extension')
|
||||
elif failure:
|
||||
dummy, ext_cert_key_usage = failure
|
||||
messages.append(
|
||||
'Invalid extendedKeyUsage component (got %s, expected all of %s to be present)' % (ext_cert_key_usage, self.extended_key_usage)
|
||||
)
|
||||
|
||||
if self.subject_alt_name is not None:
|
||||
failure = self._validate_subject_alt_name()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no subjectAltName extension')
|
||||
elif failure:
|
||||
dummy, cert_san = failure
|
||||
messages.append(
|
||||
'Invalid subjectAltName component (got %s, expected all of %s to be present)' %
|
||||
(cert_san, self.subject_alt_name)
|
||||
)
|
||||
|
||||
if self.not_before is not None:
|
||||
cert_not_valid_before = self._validate_not_before()
|
||||
if cert_not_valid_before != get_relative_time_option(self.not_before, 'not_before', backend=self.backend):
|
||||
messages.append(
|
||||
'Invalid not_before component (got %s, expected %s to be present)' %
|
||||
(cert_not_valid_before, self.not_before)
|
||||
)
|
||||
|
||||
if self.not_after is not None:
|
||||
cert_not_valid_after = self._validate_not_after()
|
||||
if cert_not_valid_after != get_relative_time_option(self.not_after, 'not_after', backend=self.backend):
|
||||
messages.append(
|
||||
'Invalid not_after component (got %s, expected %s to be present)' %
|
||||
(cert_not_valid_after, self.not_after)
|
||||
)
|
||||
|
||||
if self.valid_at is not None:
|
||||
not_before, valid_at, not_after = self._validate_valid_at()
|
||||
if not (not_before <= valid_at <= not_after):
|
||||
messages.append(
|
||||
'Certificate is not valid for the specified date (%s) - not_before: %s - not_after: %s' %
|
||||
(self.valid_at, not_before, not_after)
|
||||
)
|
||||
|
||||
if self.invalid_at is not None:
|
||||
not_before, invalid_at, not_after = self._validate_invalid_at()
|
||||
if not_before <= invalid_at <= not_after:
|
||||
messages.append(
|
||||
'Certificate is not invalid for the specified date (%s) - not_before: %s - not_after: %s' %
|
||||
(self.invalid_at, not_before, not_after)
|
||||
)
|
||||
|
||||
if self.valid_in is not None:
|
||||
not_before, valid_in, not_after = self._validate_valid_in()
|
||||
if not not_before <= valid_in <= not_after:
|
||||
messages.append(
|
||||
'Certificate is not valid in %s from now (that would be %s) - not_before: %s - not_after: %s' %
|
||||
(self.valid_in, valid_in, not_before, not_after)
|
||||
)
|
||||
return messages
|
||||
|
||||
def needs_regeneration(self):
|
||||
self._ensure_existing_certificate_loaded()
|
||||
if self.existing_certificate is None:
|
||||
self.messages = ['Certificate not provided']
|
||||
else:
|
||||
self.messages = self.assertonly()
|
||||
|
||||
return len(self.messages) != 0
|
||||
|
||||
def generate_certificate(self):
|
||||
self.module.fail_json(msg=' | '.join(self.messages))
|
||||
|
||||
def get_certificate_data(self):
|
||||
return self.existing_certificate_bytes
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackendCryptography(AssertOnlyCertificateBackend):
|
||||
"""Validate the supplied cert, using the cryptography backend"""
|
||||
def __init__(self, module):
|
||||
super(AssertOnlyCertificateBackendCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def _validate_privatekey(self):
|
||||
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
|
||||
|
||||
def _validate_csr_signature(self):
|
||||
if not self.csr.is_signature_valid:
|
||||
return False
|
||||
return cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key())
|
||||
|
||||
def _validate_csr_subject(self):
|
||||
return self.csr.subject == self.existing_certificate.subject
|
||||
|
||||
def _validate_csr_extensions(self):
|
||||
cert_exts = self.existing_certificate.extensions
|
||||
csr_exts = self.csr.extensions
|
||||
if len(cert_exts) != len(csr_exts):
|
||||
return False
|
||||
for cert_ext in cert_exts:
|
||||
try:
|
||||
csr_ext = csr_exts.get_extension_for_oid(cert_ext.oid)
|
||||
if cert_ext != csr_ext:
|
||||
return False
|
||||
except cryptography.x509.ExtensionNotFound as dummy:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_signature_algorithms(self):
|
||||
if self.existing_certificate.signature_algorithm_oid._name not in self.signature_algorithms:
|
||||
return self.existing_certificate.signature_algorithm_oid._name
|
||||
|
||||
def _validate_subject(self):
|
||||
expected_subject = Name([NameAttribute(oid=cryptography_name_to_oid(sub[0]), value=to_text(sub[1]))
|
||||
for sub in self.subject])
|
||||
cert_subject = self.existing_certificate.subject
|
||||
if not compare_sets(expected_subject, cert_subject, self.subject_strict):
|
||||
return expected_subject, cert_subject
|
||||
|
||||
def _validate_issuer(self):
|
||||
expected_issuer = Name([NameAttribute(oid=cryptography_name_to_oid(iss[0]), value=to_text(iss[1]))
|
||||
for iss in self.issuer])
|
||||
cert_issuer = self.existing_certificate.issuer
|
||||
if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict):
|
||||
return self.issuer, cert_issuer
|
||||
|
||||
def _validate_has_expired(self):
|
||||
cert_not_after = self.existing_certificate.not_valid_after
|
||||
cert_expired = cert_not_after < datetime.datetime.utcnow()
|
||||
return cert_expired
|
||||
|
||||
def _validate_version(self):
|
||||
if self.existing_certificate.version == x509.Version.v1:
|
||||
return 1
|
||||
if self.existing_certificate.version == x509.Version.v3:
|
||||
return 3
|
||||
return "unknown"
|
||||
|
||||
def _validate_key_usage(self):
|
||||
try:
|
||||
current_key_usage = self.existing_certificate.extensions.get_extension_for_class(x509.KeyUsage).value
|
||||
test_key_usage = dict(
|
||||
digital_signature=current_key_usage.digital_signature,
|
||||
content_commitment=current_key_usage.content_commitment,
|
||||
key_encipherment=current_key_usage.key_encipherment,
|
||||
data_encipherment=current_key_usage.data_encipherment,
|
||||
key_agreement=current_key_usage.key_agreement,
|
||||
key_cert_sign=current_key_usage.key_cert_sign,
|
||||
crl_sign=current_key_usage.crl_sign,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
)
|
||||
if test_key_usage['key_agreement']:
|
||||
test_key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usages = cryptography_parse_key_usage_params(self.key_usage)
|
||||
if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict):
|
||||
return self.key_usage, [k for k, v in test_key_usage.items() if v is True]
|
||||
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_extended_key_usage(self):
|
||||
try:
|
||||
current_ext_keyusage = self.existing_certificate.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
|
||||
usages = [cryptography_name_to_oid(usage) for usage in self.extended_key_usage]
|
||||
expected_ext_keyusage = x509.ExtendedKeyUsage(usages)
|
||||
if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict):
|
||||
return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage]
|
||||
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.extended_key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_subject_alt_name(self):
|
||||
try:
|
||||
current_san = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
|
||||
expected_san = [cryptography_get_name(san) for san in self.subject_alt_name]
|
||||
if not compare_sets(expected_san, current_san, self.subject_alt_name_strict):
|
||||
return self.subject_alt_name, current_san
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.subject_alt_name:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_not_before(self):
|
||||
return self.existing_certificate.not_valid_before
|
||||
|
||||
def _validate_not_after(self):
|
||||
return self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_valid_at(self):
|
||||
rt = get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_invalid_at(self):
|
||||
rt = get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_valid_in(self):
|
||||
valid_in_date = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, valid_in_date, self.existing_certificate.not_valid_after
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackendPyOpenSSL(AssertOnlyCertificateBackend):
|
||||
"""validate the supplied certificate."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(AssertOnlyCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
# Ensure inputs are properly sanitized before comparison.
|
||||
for param in ['signature_algorithms', 'key_usage', 'extended_key_usage',
|
||||
'subject_alt_name', 'subject', 'issuer', 'not_before',
|
||||
'not_after', 'valid_at', 'invalid_at']:
|
||||
attr = getattr(self, param)
|
||||
if isinstance(attr, list) and attr:
|
||||
if isinstance(attr[0], str):
|
||||
setattr(self, param, [to_bytes(item) for item in attr])
|
||||
elif isinstance(attr[0], tuple):
|
||||
setattr(self, param, [(to_bytes(item[0]), to_bytes(item[1])) for item in attr])
|
||||
elif isinstance(attr, tuple):
|
||||
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
|
||||
elif isinstance(attr, dict):
|
||||
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
|
||||
elif isinstance(attr, str):
|
||||
setattr(self, param, to_bytes(attr))
|
||||
|
||||
def _validate_privatekey(self):
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
|
||||
try:
|
||||
ctx.use_privatekey(self.privatekey)
|
||||
ctx.use_certificate(self.existing_certificate)
|
||||
except OpenSSL.SSL.Error as exc:
|
||||
raise OpenSSLObjectError('Unexpected error while trying to validate private key with certificate: %s' % exc)
|
||||
try:
|
||||
ctx.check_privatekey()
|
||||
return True
|
||||
except OpenSSL.SSL.Error:
|
||||
return False
|
||||
|
||||
def _validate_csr_signature(self):
|
||||
try:
|
||||
self.csr.verify(self.existing_certificate.get_pubkey())
|
||||
except OpenSSL.crypto.Error:
|
||||
return False
|
||||
|
||||
def _validate_csr_subject(self):
|
||||
if self.csr.get_subject() != self.existing_certificate.get_subject():
|
||||
return False
|
||||
|
||||
def _validate_csr_extensions(self):
|
||||
csr_extensions = self.csr.get_extensions()
|
||||
cert_extension_count = self.existing_certificate.get_extension_count()
|
||||
if len(csr_extensions) != cert_extension_count:
|
||||
return False
|
||||
for extension_number in range(0, cert_extension_count):
|
||||
cert_extension = self.existing_certificate.get_extension(extension_number)
|
||||
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
|
||||
if cert_extension.get_data() != list(csr_extension)[0].get_data():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_signature_algorithms(self):
|
||||
if self.existing_certificate.get_signature_algorithm() not in self.signature_algorithms:
|
||||
return self.existing_certificate.get_signature_algorithm()
|
||||
|
||||
def _validate_subject(self):
|
||||
expected_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in self.subject]
|
||||
cert_subject = self.existing_certificate.get_subject().get_components()
|
||||
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in cert_subject]
|
||||
if not compare_sets(expected_subject, current_subject, self.subject_strict):
|
||||
return expected_subject, current_subject
|
||||
|
||||
def _validate_issuer(self):
|
||||
expected_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in self.issuer]
|
||||
cert_issuer = self.existing_certificate.get_issuer().get_components()
|
||||
current_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in cert_issuer]
|
||||
if not compare_sets(expected_issuer, current_issuer, self.issuer_strict):
|
||||
return self.issuer, cert_issuer
|
||||
|
||||
def _validate_has_expired(self):
|
||||
# The following 3 lines are the same as the current PyOpenSSL code for cert.has_expired().
|
||||
# Older version of PyOpenSSL have a buggy implementation,
|
||||
# to avoid issues with those we added the code from a more recent release here.
|
||||
|
||||
time_string = to_native(self.existing_certificate.get_notAfter())
|
||||
not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
cert_expired = not_after < datetime.datetime.utcnow()
|
||||
return cert_expired
|
||||
|
||||
def _validate_version(self):
|
||||
# Version numbers in certs are off by one:
|
||||
# v1: 0, v2: 1, v3: 2 ...
|
||||
return self.existing_certificate.get_version() + 1
|
||||
|
||||
def _validate_key_usage(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'keyUsage':
|
||||
found = True
|
||||
expected_extension = crypto.X509Extension(b"keyUsage", False, b', '.join(self.key_usage))
|
||||
key_usage = [usage.strip() for usage in to_text(expected_extension, errors='surrogate_or_strict').split(',')]
|
||||
current_ku = [usage.strip() for usage in to_text(extension, errors='surrogate_or_strict').split(',')]
|
||||
if not compare_sets(key_usage, current_ku, self.key_usage_strict):
|
||||
return self.key_usage, str(extension).split(', ')
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_extended_key_usage(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'extendedKeyUsage':
|
||||
found = True
|
||||
extKeyUsage = [OpenSSL._util.lib.OBJ_txt2nid(keyUsage) for keyUsage in self.extended_key_usage]
|
||||
current_xku = [OpenSSL._util.lib.OBJ_txt2nid(usage.strip()) for usage in
|
||||
to_bytes(extension, errors='surrogate_or_strict').split(b',')]
|
||||
if not compare_sets(extKeyUsage, current_xku, self.extended_key_usage_strict):
|
||||
return self.extended_key_usage, str(extension).split(', ')
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.extended_key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_subject_alt_name(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
found = True
|
||||
l_altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
sans = [pyopenssl_normalize_name_attribute(to_text(san, errors='surrogate_or_strict')) for san in self.subject_alt_name]
|
||||
if not compare_sets(sans, l_altnames, self.subject_alt_name_strict):
|
||||
return self.subject_alt_name, l_altnames
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.subject_alt_name:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_not_before(self):
|
||||
return self.existing_certificate.get_notBefore()
|
||||
|
||||
def _validate_not_after(self):
|
||||
return self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_valid_at(self):
|
||||
rt = get_relative_time_option(self.valid_at, "valid_at", backend=self.backend)
|
||||
rt = to_bytes(rt, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_invalid_at(self):
|
||||
rt = get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend)
|
||||
rt = to_bytes(rt, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_valid_in(self):
|
||||
valid_in_asn1 = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
|
||||
valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), valid_in_date, self.existing_certificate.get_notAfter()
|
||||
|
||||
|
||||
class AssertOnlyCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
module.deprecate("The 'assertonly' provider is deprecated; please see the examples of "
|
||||
"the 'x509_certificate' module on how to replace it with other modules",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return False
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return AssertOnlyCertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return AssertOnlyCertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_assertonly_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('assertonly')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
signature_algorithms=dict(type='list', elements='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
issuer=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
issuer_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
has_expired=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
version=dict(type='int', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
key_usage=dict(type='list', elements='str', aliases=['keyUsage'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
valid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
invalid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
valid_in=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
))
|
||||
@@ -0,0 +1,225 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import os
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_certificate,
|
||||
get_relative_time_option,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_serial_number_of_cert,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
from cryptography.x509.oid import NameOID
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if self.csr_content is None and self.csr_path is None:
|
||||
raise CertificateError(
|
||||
'csr_path or csr_content is required for entrust provider'
|
||||
)
|
||||
if self.csr_content is None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
|
||||
# ECS API defaults to using the validated organization tied to the account.
|
||||
# We want to always force behavior of trying to use the organization provided in the CSR.
|
||||
# To that end we need to parse out the organization from the CSR.
|
||||
self.csr_org = None
|
||||
if self.backend == 'pyopenssl':
|
||||
csr_subject = self.csr.get_subject()
|
||||
csr_subject_components = csr_subject.get_components()
|
||||
for k, v in csr_subject_components:
|
||||
if k.upper() == 'O':
|
||||
# Entrust does not support multiple validated organizations in a single certificate
|
||||
if self.csr_org is not None:
|
||||
self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations "
|
||||
"found in Subject DN: '{0}'. ".format(csr_subject)))
|
||||
else:
|
||||
self.csr_org = v
|
||||
elif self.backend == 'cryptography':
|
||||
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
|
||||
if len(csr_subject_orgs) == 1:
|
||||
self.csr_org = csr_subject_orgs[0].value
|
||||
elif len(csr_subject_orgs) > 1:
|
||||
self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
|
||||
"Subject DN: '{0}'. ".format(self.csr.subject)))
|
||||
# If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to
|
||||
# organization tied to the account.
|
||||
if self.csr_org is None:
|
||||
self.csr_org = ''
|
||||
|
||||
try:
|
||||
self.ecs_client = ECSClient(
|
||||
entrust_api_user=self.module.params['entrust_api_user'],
|
||||
entrust_api_key=self.module.params['entrust_api_key'],
|
||||
entrust_api_cert=self.module.params['entrust_api_client_cert_path'],
|
||||
entrust_api_cert_key=self.module.params['entrust_api_client_cert_key_path'],
|
||||
entrust_api_specification_path=self.module.params['entrust_api_specification_path']
|
||||
)
|
||||
except SessionConfigurationException as e:
|
||||
module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message)))
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
body = {}
|
||||
|
||||
# Read the CSR that was generated for us
|
||||
if self.csr_content is not None:
|
||||
# csr_content contains bytes
|
||||
body['csr'] = to_native(self.csr_content)
|
||||
else:
|
||||
with open(self.csr_path, 'r') as csr_file:
|
||||
body['csr'] = csr_file.read()
|
||||
|
||||
body['certType'] = self.module.params['entrust_cert_type']
|
||||
|
||||
# Handle expiration (30 days if not specified)
|
||||
expiry = self.notAfter
|
||||
if not expiry:
|
||||
gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
|
||||
expiry = gmt_now + datetime.timedelta(days=365)
|
||||
|
||||
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
|
||||
body['certExpiryDate'] = expiry_iso3339
|
||||
body['org'] = self.csr_org
|
||||
body['tracking'] = {
|
||||
'requesterName': self.module.params['entrust_requester_name'],
|
||||
'requesterEmail': self.module.params['entrust_requester_email'],
|
||||
'requesterPhone': self.module.params['entrust_requester_phone'],
|
||||
}
|
||||
|
||||
try:
|
||||
result = self.ecs_client.NewCertRequest(Body=body)
|
||||
self.trackingId = result.get('trackingId')
|
||||
except RestOperationException as e:
|
||||
self.module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message)))
|
||||
|
||||
self.cert_bytes = to_bytes(result.get('endEntityCert'))
|
||||
self.cert = load_certificate(path=None, content=self.cert_bytes, backend=self.backend)
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert_bytes
|
||||
|
||||
def needs_regeneration(self):
|
||||
parent_check = super(EntrustCertificateBackend, self).needs_regeneration()
|
||||
|
||||
try:
|
||||
cert_details = self._get_cert_details()
|
||||
except RestOperationException as e:
|
||||
self.module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message)))
|
||||
|
||||
# Always issue a new certificate if the certificate is expired, suspended or revoked
|
||||
status = cert_details.get('status', False)
|
||||
if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED':
|
||||
return True
|
||||
|
||||
# If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed
|
||||
if self.module.params['entrust_cert_type'] and cert_details.get('certType') and self.module.params['entrust_cert_type'] != cert_details.get('certType'):
|
||||
return True
|
||||
|
||||
return parent_check
|
||||
|
||||
def _get_cert_details(self):
|
||||
cert_details = {}
|
||||
try:
|
||||
self._ensure_existing_certificate_loaded()
|
||||
except Exception as dummy:
|
||||
return
|
||||
if self.existing_certificate:
|
||||
serial_number = None
|
||||
expiry = None
|
||||
if self.backend == 'pyopenssl':
|
||||
serial_number = "{0:X}".format(self.existing_certificate.get_serial_number())
|
||||
time_string = to_native(self.existing_certificate.get_notAfter())
|
||||
expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
elif self.backend == 'cryptography':
|
||||
serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate))
|
||||
expiry = self.existing_certificate.not_valid_after
|
||||
|
||||
# get some information about the expiry of this certificate
|
||||
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
|
||||
cert_details['expiresAfter'] = expiry_iso3339
|
||||
|
||||
# If a trackingId is not already defined (from the result of a generate)
|
||||
# use the serial number to identify the tracking Id
|
||||
if self.trackingId is None and serial_number is not None:
|
||||
cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {})
|
||||
|
||||
# Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks
|
||||
# on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is
|
||||
# still checked as it is in the rest of the module.
|
||||
if len(cert_results) == 1:
|
||||
self.trackingId = cert_results[0].get('trackingId')
|
||||
|
||||
if self.trackingId is not None:
|
||||
cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId))
|
||||
|
||||
return cert_details
|
||||
|
||||
|
||||
class EntrustCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
pass
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return False
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
return EntrustCertificateBackend(module, backend)
|
||||
|
||||
|
||||
def add_entrust_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('entrust')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
entrust_cert_type=dict(type='str', default='STANDARD_SSL',
|
||||
choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL',
|
||||
'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']),
|
||||
entrust_requester_email=dict(type='str'),
|
||||
entrust_requester_name=dict(type='str'),
|
||||
entrust_requester_phone=dict(type='str'),
|
||||
entrust_api_user=dict(type='str'),
|
||||
entrust_api_key=dict(type='str', no_log=True),
|
||||
entrust_api_client_cert_path=dict(type='path'),
|
||||
entrust_api_client_cert_key_path=dict(type='path', no_log=True),
|
||||
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'),
|
||||
entrust_not_after=dict(type='str', default='+365d'),
|
||||
))
|
||||
argument_spec.required_if.append(
|
||||
['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone',
|
||||
'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path',
|
||||
'entrust_api_client_cert_key_path']]
|
||||
)
|
||||
565
plugins/module_utils/crypto/module_backends/certificate_info.py
Normal file
565
plugins/module_utils/crypto/module_backends/certificate_info.py
Normal file
@@ -0,0 +1,565 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import datetime
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_certificate,
|
||||
get_fingerprint_of_bytes,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_decode_name,
|
||||
cryptography_get_extensions_from_cert,
|
||||
cryptography_oid_to_name,
|
||||
cryptography_serial_number_of_cert,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_get_extensions_from_cert,
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
get_publickey_info,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CertificateInfoRetrieval(object):
|
||||
def __init__(self, module, backend, content):
|
||||
# content must be a bytes string
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.content = content
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_der_bytes(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_signature_algorithm(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_ordered(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_issuer_ordered(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_version(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_basic_constraints(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ocsp_must_staple(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_not_before(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_not_after(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key_pem(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key_object(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_authority_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_serial_number(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_all_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ocsp_uri(self):
|
||||
pass
|
||||
|
||||
def get_info(self, prefer_one_fingerprint=False):
|
||||
result = dict()
|
||||
self.cert = load_certificate(None, content=self.content, backend=self.backend)
|
||||
|
||||
result['signature_algorithm'] = self._get_signature_algorithm()
|
||||
subject = self._get_subject_ordered()
|
||||
issuer = self._get_issuer_ordered()
|
||||
result['subject'] = dict()
|
||||
for k, v in subject:
|
||||
result['subject'][k] = v
|
||||
result['subject_ordered'] = subject
|
||||
result['issuer'] = dict()
|
||||
for k, v in issuer:
|
||||
result['issuer'][k] = v
|
||||
result['issuer_ordered'] = issuer
|
||||
result['version'] = self._get_version()
|
||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
||||
|
||||
not_before = self.get_not_before()
|
||||
not_after = self.get_not_after()
|
||||
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
|
||||
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
|
||||
result['expired'] = not_after < datetime.datetime.utcnow()
|
||||
|
||||
result['public_key'] = self._get_public_key_pem()
|
||||
|
||||
public_key_info = get_publickey_info(
|
||||
self.module,
|
||||
self.backend,
|
||||
key=self._get_public_key_object(),
|
||||
prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
result.update({
|
||||
'public_key_type': public_key_info['type'],
|
||||
'public_key_data': public_key_info['public_data'],
|
||||
'public_key_fingerprints': public_key_info['fingerprints'],
|
||||
})
|
||||
|
||||
result['fingerprints'] = get_fingerprint_of_bytes(
|
||||
self._get_der_bytes(), prefer_one=prefer_one_fingerprint)
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
|
||||
result['serial_number'] = self._get_serial_number()
|
||||
result['extensions_by_oid'] = self._get_all_extensions()
|
||||
result['ocsp_uri'] = self._get_ocsp_uri()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
|
||||
"""Validate the supplied cert, using the cryptography backend"""
|
||||
def __init__(self, module, content):
|
||||
super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content)
|
||||
|
||||
def _get_der_bytes(self):
|
||||
return self.cert.public_bytes(serialization.Encoding.DER)
|
||||
|
||||
def _get_signature_algorithm(self):
|
||||
return cryptography_oid_to_name(self.cert.signature_algorithm_oid)
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
result = []
|
||||
for attribute in self.cert.subject:
|
||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
return result
|
||||
|
||||
def _get_issuer_ordered(self):
|
||||
result = []
|
||||
for attribute in self.cert.issuer:
|
||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
return result
|
||||
|
||||
def _get_version(self):
|
||||
if self.cert.version == x509.Version.v1:
|
||||
return 1
|
||||
if self.cert.version == x509.Version.v3:
|
||||
return 3
|
||||
return "unknown"
|
||||
|
||||
def _get_key_usage(self):
|
||||
try:
|
||||
current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage)
|
||||
current_key_usage = current_key_ext.value
|
||||
key_usage = dict(
|
||||
digital_signature=current_key_usage.digital_signature,
|
||||
content_commitment=current_key_usage.content_commitment,
|
||||
key_encipherment=current_key_usage.key_encipherment,
|
||||
data_encipherment=current_key_usage.data_encipherment,
|
||||
key_agreement=current_key_usage.key_agreement,
|
||||
key_cert_sign=current_key_usage.key_cert_sign,
|
||||
crl_sign=current_key_usage.crl_sign,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
)
|
||||
if key_usage['key_agreement']:
|
||||
key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usage_names = dict(
|
||||
digital_signature='Digital Signature',
|
||||
content_commitment='Non Repudiation',
|
||||
key_encipherment='Key Encipherment',
|
||||
data_encipherment='Data Encipherment',
|
||||
key_agreement='Key Agreement',
|
||||
key_cert_sign='Certificate Sign',
|
||||
crl_sign='CRL Sign',
|
||||
encipher_only='Encipher Only',
|
||||
decipher_only='Decipher Only',
|
||||
)
|
||||
return sorted([
|
||||
key_usage_names[name] for name, value in key_usage.items() if value
|
||||
]), current_key_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
||||
return sorted([
|
||||
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
||||
]), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints)
|
||||
result = []
|
||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
||||
if ext_keyusage_ext.value.path_length is not None:
|
||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
||||
return sorted(result), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
try:
|
||||
try:
|
||||
# This only works with cryptography >= 2.1
|
||||
tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature)
|
||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||
except AttributeError:
|
||||
# Fallback for cryptography < 2.1
|
||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||
tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid)
|
||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
||||
return value, tlsfeature_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
try:
|
||||
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
||||
return result, san_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def get_not_before(self):
|
||||
return self.cert.not_valid_before
|
||||
|
||||
def get_not_after(self):
|
||||
return self.cert.not_valid_after
|
||||
|
||||
def _get_public_key_pem(self):
|
||||
return self.cert.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
|
||||
def _get_public_key_object(self):
|
||||
return self.cert.public_key()
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
try:
|
||||
ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
return ext.value.digest
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
try:
|
||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||
issuer = None
|
||||
if ext.value.authority_cert_issuer is not None:
|
||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, None, None
|
||||
|
||||
def _get_serial_number(self):
|
||||
return cryptography_serial_number_of_cert(self.cert)
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return cryptography_get_extensions_from_cert(self.cert)
|
||||
|
||||
def _get_ocsp_uri(self):
|
||||
try:
|
||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
|
||||
for desc in ext.value:
|
||||
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
|
||||
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
|
||||
return desc.access_location.value
|
||||
except x509.ExtensionNotFound as dummy:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class CertificateInfoRetrievalPyOpenSSL(CertificateInfoRetrieval):
|
||||
"""validate the supplied certificate."""
|
||||
|
||||
def __init__(self, module, content):
|
||||
super(CertificateInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content)
|
||||
|
||||
def _get_der_bytes(self):
|
||||
return crypto.dump_certificate(crypto.FILETYPE_ASN1, self.cert)
|
||||
|
||||
def _get_signature_algorithm(self):
|
||||
return to_text(self.cert.get_signature_algorithm())
|
||||
|
||||
def __get_name(self, name):
|
||||
result = []
|
||||
for sub in name.get_components():
|
||||
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
||||
return result
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
return self.__get_name(self.cert.get_subject())
|
||||
|
||||
def _get_issuer_ordered(self):
|
||||
return self.__get_name(self.cert.get_issuer())
|
||||
|
||||
def _get_version(self):
|
||||
# Version numbers in certs are off by one:
|
||||
# v1: 0, v2: 1, v3: 2 ...
|
||||
return self.cert.get_version() + 1
|
||||
|
||||
def _get_extension(self, short_name):
|
||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
||||
extension = self.cert.get_extension(extension_idx)
|
||||
if extension.get_short_name() == short_name:
|
||||
result = [
|
||||
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
||||
]
|
||||
return sorted(result), bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_key_usage(self):
|
||||
return self._get_extension(b'keyUsage')
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
return self._get_extension(b'extendedKeyUsage')
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
return self._get_extension(b'basicConstraints')
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())]
|
||||
oms_ext = [
|
||||
ext for ext in extensions
|
||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
||||
]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if oms_ext:
|
||||
return True, bool(oms_ext[0].get_critical())
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
||||
extension = self.cert.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
return result, bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def get_not_before(self):
|
||||
time_string = to_native(self.cert.get_notBefore())
|
||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def get_not_after(self):
|
||||
time_string = to_native(self.cert.get_notAfter())
|
||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def _get_public_key_pem(self):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_PEM,
|
||||
self.cert.get_pubkey(),
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_public_key_object(self):
|
||||
return self.cert.get_pubkey()
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None, None, None
|
||||
|
||||
def _get_serial_number(self):
|
||||
return self.cert.get_serial_number()
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return pyopenssl_get_extensions_from_cert(self.cert)
|
||||
|
||||
def _get_ocsp_uri(self):
|
||||
for i in range(self.cert.get_extension_count()):
|
||||
ext = self.cert.get_extension(i)
|
||||
if ext.get_short_name() == b'authorityInfoAccess':
|
||||
v = str(ext)
|
||||
m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_certificate_info(module, backend, content, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = CertificateInfoRetrievalCryptography(module, content)
|
||||
elif backend == 'pyopenssl':
|
||||
info = CertificateInfoRetrievalPyOpenSSL(module, content)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
def select_backend(module, backend, content):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CertificateInfoRetrievalPyOpenSSL(module, content)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, CertificateInfoRetrievalCryptography(module, content)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
390
plugins/module_utils/crypto/module_backends/certificate_ownca.py
Normal file
390
plugins/module_utils/crypto/module_backends/certificate_ownca.py
Normal file
@@ -0,0 +1,390 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
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_compare_public_keys,
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_serial_number_of_cert,
|
||||
cryptography_verify_certificate_signature,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class OwnCACertificateBackendCryptography(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(OwnCACertificateBackendCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
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.digest = select_message_digest(module.params['ownca_digest'])
|
||||
self.version = module.params['ownca_version']
|
||||
self.serial_number = x509.random_serial_number()
|
||||
self.ca_cert_path = module.params['ownca_path']
|
||||
self.ca_cert_content = module.params['ownca_content']
|
||||
if self.ca_cert_content is not None:
|
||||
self.ca_cert_content = self.ca_cert_content.encode('utf-8')
|
||||
self.ca_privatekey_path = module.params['ownca_privatekey_path']
|
||||
self.ca_privatekey_content = module.params['ownca_privatekey_content']
|
||||
if self.ca_privatekey_content is not None:
|
||||
self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8')
|
||||
self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase']
|
||||
|
||||
if self.csr_content is None and self.csr_path is None:
|
||||
raise CertificateError(
|
||||
'csr_path or csr_content is required for ownca provider'
|
||||
)
|
||||
if self.csr_content is None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
|
||||
raise CertificateError(
|
||||
'The CA certificate file {0} does not exist'.format(self.ca_cert_path)
|
||||
)
|
||||
if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path):
|
||||
raise CertificateError(
|
||||
'The CA private key file {0} does not exist'.format(self.ca_privatekey_path)
|
||||
)
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
self.ca_cert = load_certificate(
|
||||
path=self.ca_cert_path,
|
||||
content=self.ca_cert_content,
|
||||
backend=self.backend
|
||||
)
|
||||
try:
|
||||
self.ca_private_key = load_privatekey(
|
||||
path=self.ca_privatekey_path,
|
||||
content=self.ca_privatekey_content,
|
||||
passphrase=self.ca_privatekey_passphrase,
|
||||
backend=self.backend
|
||||
)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
module.fail_json(msg=str(exc))
|
||||
|
||||
if not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()):
|
||||
raise CertificateError('The CA private key does not belong to the CA certificate')
|
||||
|
||||
if cryptography_key_needs_digest_for_signing(self.ca_private_key):
|
||||
if self.digest is None:
|
||||
raise CertificateError(
|
||||
'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest']
|
||||
)
|
||||
else:
|
||||
self.digest = None
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert_builder = x509.CertificateBuilder()
|
||||
cert_builder = cert_builder.subject_name(self.csr.subject)
|
||||
cert_builder = cert_builder.issuer_name(self.ca_cert.subject)
|
||||
cert_builder = cert_builder.serial_number(self.serial_number)
|
||||
cert_builder = cert_builder.not_valid_before(self.notBefore)
|
||||
cert_builder = cert_builder.not_valid_after(self.notAfter)
|
||||
cert_builder = cert_builder.public_key(self.csr.public_key())
|
||||
has_ski = False
|
||||
for extension in self.csr.extensions:
|
||||
if isinstance(extension.value, x509.SubjectKeyIdentifier):
|
||||
if self.create_subject_key_identifier == 'always_create':
|
||||
continue
|
||||
has_ski = True
|
||||
if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier):
|
||||
continue
|
||||
cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical)
|
||||
if not has_ski and self.create_subject_key_identifier != 'never_create':
|
||||
cert_builder = cert_builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()),
|
||||
critical=False
|
||||
)
|
||||
if self.create_authority_key_identifier:
|
||||
try:
|
||||
ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
cert_builder = cert_builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value)
|
||||
if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else
|
||||
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext),
|
||||
critical=False
|
||||
)
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
cert_builder = cert_builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
try:
|
||||
certificate = cert_builder.sign(
|
||||
private_key=self.ca_private_key, algorithm=self.digest,
|
||||
backend=default_backend()
|
||||
)
|
||||
except TypeError as e:
|
||||
if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None:
|
||||
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
|
||||
raise
|
||||
|
||||
self.cert = certificate
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert.public_bytes(Encoding.PEM)
|
||||
|
||||
def needs_regeneration(self):
|
||||
if super(OwnCACertificateBackendCryptography, self).needs_regeneration():
|
||||
return True
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
|
||||
# Check whether certificate is signed by CA certificate
|
||||
if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()):
|
||||
return True
|
||||
|
||||
# Check subject
|
||||
if self.ca_cert.subject != self.existing_certificate.issuer:
|
||||
return True
|
||||
|
||||
# Check AuthorityKeyIdentifier
|
||||
if self.create_authority_key_identifier:
|
||||
try:
|
||||
ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
expected_ext = (
|
||||
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value)
|
||||
if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else
|
||||
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext)
|
||||
)
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key())
|
||||
|
||||
try:
|
||||
ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||
if ext.value != expected_ext:
|
||||
return True
|
||||
except cryptography.x509.ExtensionNotFound as dummy:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(OwnCACertificateBackendCryptography, self).dump(include_certificate)
|
||||
result.update({
|
||||
'ca_cert': self.ca_cert_path,
|
||||
'ca_privatekey': self.ca_privatekey_path,
|
||||
})
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"),
|
||||
'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"),
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
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"),
|
||||
'serial_number': cryptography_serial_number_of_cert(self.cert),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_serial_number():
|
||||
"""Generate a serial number for a certificate"""
|
||||
while True:
|
||||
result = randrange(0, 1 << 160)
|
||||
if result >= 1000:
|
||||
return result
|
||||
|
||||
|
||||
class OwnCACertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(OwnCACertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
self.notBefore = get_relative_time_option(self.module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(self.module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
|
||||
self.digest = self.module.params['ownca_digest']
|
||||
self.version = self.module.params['ownca_version']
|
||||
self.serial_number = generate_serial_number()
|
||||
if self.module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
self.module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
if self.module.params['ownca_create_authority_key_identifier']:
|
||||
self.module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!')
|
||||
self.ca_cert_path = self.module.params['ownca_path']
|
||||
self.ca_cert_content = self.module.params['ownca_content']
|
||||
if self.ca_cert_content is not None:
|
||||
self.ca_cert_content = self.ca_cert_content.encode('utf-8')
|
||||
self.ca_privatekey_path = self.module.params['ownca_privatekey_path']
|
||||
self.ca_privatekey_content = self.module.params['ownca_privatekey_content']
|
||||
if self.ca_privatekey_content is not None:
|
||||
self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8')
|
||||
self.ca_privatekey_passphrase = self.module.params['ownca_privatekey_passphrase']
|
||||
|
||||
if self.csr_content is None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
|
||||
raise CertificateError(
|
||||
'The CA certificate file {0} does not exist'.format(self.ca_cert_path)
|
||||
)
|
||||
if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path):
|
||||
raise CertificateError(
|
||||
'The CA private key file {0} does not exist'.format(self.ca_privatekey_path)
|
||||
)
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
self.ca_cert = load_certificate(
|
||||
path=self.ca_cert_path,
|
||||
content=self.ca_cert_content,
|
||||
)
|
||||
try:
|
||||
self.ca_privatekey = load_privatekey(
|
||||
path=self.ca_privatekey_path,
|
||||
content=self.ca_privatekey_content,
|
||||
passphrase=self.ca_privatekey_passphrase
|
||||
)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
self.module.fail_json(msg=str(exc))
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.ca_cert.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.ca_privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def needs_regeneration(self):
|
||||
if super(OwnCACertificateBackendPyOpenSSL, self).needs_regeneration():
|
||||
return True
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
|
||||
# Check subject
|
||||
if self.ca_cert.get_subject() != self.existing_certificate.get_issuer():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(OwnCACertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
result.update({
|
||||
'ca_cert': self.ca_cert_path,
|
||||
'ca_privatekey': self.ca_privatekey_path,
|
||||
})
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class OwnCACertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
if module.params['ownca_path'] is None and module.params['ownca_content'] is None:
|
||||
module.fail_json(msg='One of ownca_path and ownca_content must be specified for the ownca provider.')
|
||||
if module.params['ownca_privatekey_path'] is None and module.params['ownca_privatekey_content'] is None:
|
||||
module.fail_json(msg='One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider.')
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return module.params['ownca_version'] == 2
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return OwnCACertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return OwnCACertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_ownca_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('ownca')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
ownca_path=dict(type='path'),
|
||||
ownca_content=dict(type='str'),
|
||||
ownca_privatekey_path=dict(type='path'),
|
||||
ownca_privatekey_content=dict(type='str', no_log=True),
|
||||
ownca_privatekey_passphrase=dict(type='str', no_log=True),
|
||||
ownca_digest=dict(type='str', default='sha256'),
|
||||
ownca_version=dict(type='int', default=3),
|
||||
ownca_not_before=dict(type='str', default='+0s'),
|
||||
ownca_not_after=dict(type='str', default='+3650d'),
|
||||
ownca_create_subject_key_identifier=dict(
|
||||
type='str',
|
||||
default='create_if_not_provided',
|
||||
choices=['create_if_not_provided', 'always_create', 'never_create']
|
||||
),
|
||||
ownca_create_authority_key_identifier=dict(type='bool', default=True),
|
||||
))
|
||||
argument_spec.mutually_exclusive.extend([
|
||||
['ownca_path', 'ownca_content'],
|
||||
['ownca_privatekey_path', 'ownca_privatekey_content'],
|
||||
])
|
||||
@@ -0,0 +1,276 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
get_relative_time_option,
|
||||
select_message_digest,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_serial_number_of_cert,
|
||||
cryptography_verify_certificate_signature,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class SelfSignedCertificateBackendCryptography(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
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.digest = select_message_digest(module.params['selfsigned_digest'])
|
||||
self.version = module.params['selfsigned_version']
|
||||
self.serial_number = x509.random_serial_number()
|
||||
|
||||
if self.csr_path is not None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
||||
raise CertificateError(
|
||||
'The private key file {0} does not exist'.format(self.privatekey_path)
|
||||
)
|
||||
|
||||
self._module = module
|
||||
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
if self.csr is None:
|
||||
# Create empty CSR on the fly
|
||||
csr = cryptography.x509.CertificateSigningRequestBuilder()
|
||||
csr = csr.subject_name(cryptography.x509.Name([]))
|
||||
digest = None
|
||||
if cryptography_key_needs_digest_for_signing(self.privatekey):
|
||||
digest = self.digest
|
||||
if digest is None:
|
||||
self.module.fail_json(msg='Unsupported digest "{0}"'.format(module.params['selfsigned_digest']))
|
||||
try:
|
||||
self.csr = csr.sign(self.privatekey, digest, default_backend())
|
||||
except TypeError as e:
|
||||
if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None:
|
||||
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
|
||||
raise
|
||||
|
||||
if cryptography_key_needs_digest_for_signing(self.privatekey):
|
||||
if self.digest is None:
|
||||
raise CertificateError(
|
||||
'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest']
|
||||
)
|
||||
else:
|
||||
self.digest = None
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
try:
|
||||
cert_builder = x509.CertificateBuilder()
|
||||
cert_builder = cert_builder.subject_name(self.csr.subject)
|
||||
cert_builder = cert_builder.issuer_name(self.csr.subject)
|
||||
cert_builder = cert_builder.serial_number(self.serial_number)
|
||||
cert_builder = cert_builder.not_valid_before(self.notBefore)
|
||||
cert_builder = cert_builder.not_valid_after(self.notAfter)
|
||||
cert_builder = cert_builder.public_key(self.privatekey.public_key())
|
||||
has_ski = False
|
||||
for extension in self.csr.extensions:
|
||||
if isinstance(extension.value, x509.SubjectKeyIdentifier):
|
||||
if self.create_subject_key_identifier == 'always_create':
|
||||
continue
|
||||
has_ski = True
|
||||
cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical)
|
||||
if not has_ski and self.create_subject_key_identifier != 'never_create':
|
||||
cert_builder = cert_builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()),
|
||||
critical=False
|
||||
)
|
||||
except ValueError as e:
|
||||
raise CertificateError(str(e))
|
||||
|
||||
try:
|
||||
certificate = cert_builder.sign(
|
||||
private_key=self.privatekey, algorithm=self.digest,
|
||||
backend=default_backend()
|
||||
)
|
||||
except TypeError as e:
|
||||
if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None:
|
||||
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
|
||||
raise
|
||||
|
||||
self.cert = certificate
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert.public_bytes(Encoding.PEM)
|
||||
|
||||
def needs_regeneration(self):
|
||||
if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration():
|
||||
return True
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
|
||||
# Check whether certificate is signed by private key
|
||||
if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate)
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"),
|
||||
'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"),
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
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"),
|
||||
'serial_number': cryptography_serial_number_of_cert(self.cert),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_serial_number():
|
||||
"""Generate a serial number for a certificate"""
|
||||
while True:
|
||||
result = randrange(0, 1 << 160)
|
||||
if result >= 1000:
|
||||
return result
|
||||
|
||||
|
||||
class SelfSignedCertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(SelfSignedCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend)
|
||||
self.digest = module.params['selfsigned_digest']
|
||||
self.version = module.params['selfsigned_version']
|
||||
self.serial_number = generate_serial_number()
|
||||
|
||||
if self.csr_path is not None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
||||
raise CertificateError(
|
||||
'The private key file {0} does not exist'.format(self.privatekey_path)
|
||||
)
|
||||
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
if self.csr is None:
|
||||
# Create empty CSR on the fly
|
||||
self.csr = crypto.X509Req()
|
||||
self.csr.set_pubkey(self.privatekey)
|
||||
self.csr.sign(self.privatekey, self.digest)
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.csr.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class SelfSignedCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None:
|
||||
module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.')
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return module.params['selfsigned_version'] == 2
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return SelfSignedCertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return SelfSignedCertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_selfsigned_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('selfsigned')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
selfsigned_version=dict(type='int', default=3),
|
||||
selfsigned_digest=dict(type='str', default='sha256'),
|
||||
selfsigned_not_before=dict(type='str', default='+0s', aliases=['selfsigned_notBefore']),
|
||||
selfsigned_not_after=dict(type='str', default='+3650d', aliases=['selfsigned_notAfter']),
|
||||
selfsigned_create_subject_key_identifier=dict(
|
||||
type='str',
|
||||
default='create_if_not_provided',
|
||||
choices=['create_if_not_provided', 'always_create', 'never_create']
|
||||
),
|
||||
))
|
||||
34
plugins/module_utils/crypto/module_backends/common.py
Normal file
34
plugins/module_utils/crypto/module_backends/common.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
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 {}
|
||||
|
||||
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)
|
||||
100
plugins/module_utils/crypto/module_backends/crl_info.py
Normal file
100
plugins/module_utils/crypto/module_backends/crl_info.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_oid_to_name,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
|
||||
TIMESTAMP_FORMAT,
|
||||
cryptography_decode_revoked_certificate,
|
||||
cryptography_dump_revoked,
|
||||
cryptography_get_signature_algorithm_oid_from_crl,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
identify_pem_format,
|
||||
)
|
||||
|
||||
# crypto_utils
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
class CRLInfoRetrieval(object):
|
||||
def __init__(self, module, content, list_revoked_certificates=True):
|
||||
# content must be a bytes string
|
||||
self.module = module
|
||||
self.content = content
|
||||
self.list_revoked_certificates = list_revoked_certificates
|
||||
|
||||
def get_info(self):
|
||||
self.crl_pem = identify_pem_format(self.content)
|
||||
try:
|
||||
if self.crl_pem:
|
||||
self.crl = x509.load_pem_x509_crl(self.content, default_backend())
|
||||
else:
|
||||
self.crl = x509.load_der_x509_crl(self.content, default_backend())
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
||||
|
||||
result = {
|
||||
'changed': False,
|
||||
'format': 'pem' if self.crl_pem else 'der',
|
||||
'last_update': None,
|
||||
'next_update': None,
|
||||
'digest': None,
|
||||
'issuer_ordered': None,
|
||||
'issuer': None,
|
||||
}
|
||||
|
||||
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
||||
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
||||
result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
|
||||
issuer = []
|
||||
for attribute in self.crl.issuer:
|
||||
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
result['issuer_ordered'] = issuer
|
||||
result['issuer'] = {}
|
||||
for k, v in issuer:
|
||||
result['issuer'][k] = v
|
||||
if self.list_revoked_certificates:
|
||||
result['revoked_certificates'] = []
|
||||
for cert in self.crl:
|
||||
entry = cryptography_decode_revoked_certificate(cert)
|
||||
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_crl_info(module, content, list_revoked_certificates=True):
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
info = CRLInfoRetrieval(module, content, list_revoked_certificates=list_revoked_certificates)
|
||||
return info.get_info()
|
||||
874
plugins/module_utils/crypto/module_backends/csr.py
Normal file
874
plugins/module_utils/crypto/module_backends/csr.py
Normal file
@@ -0,0 +1,874 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_privatekey,
|
||||
load_certificate_request,
|
||||
parse_name_field,
|
||||
select_message_digest,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_get_basic_constraints,
|
||||
cryptography_get_name,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_parse_key_usage_params,
|
||||
cryptography_parse_relative_distinguished_name,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
|
||||
REVOCATION_REASON_MAP,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_normalize_name_attribute,
|
||||
pyopenssl_parse_name_constraints,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
|
||||
get_csr_info,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
import cryptography.exceptions
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.hazmat.primitives.hashes
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
CRYPTOGRAPHY_MUST_STAPLE_NAME = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||
CRYPTOGRAPHY_MUST_STAPLE_VALUE = b"\x30\x03\x02\x01\x05"
|
||||
|
||||
|
||||
class CertificateSigningRequestError(OpenSSLObjectError):
|
||||
pass
|
||||
|
||||
|
||||
# From the object called `module`, only the following properties are used:
|
||||
#
|
||||
# - module.params[]
|
||||
# - module.warn(msg: str)
|
||||
# - module.fail_json(msg: str, **kwargs)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CertificateSigningRequestBackend(object):
|
||||
def __init__(self, module, backend):
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.digest = module.params['digest']
|
||||
self.privatekey_path = module.params['privatekey_path']
|
||||
self.privatekey_content = module.params['privatekey_content']
|
||||
if self.privatekey_content is not None:
|
||||
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||
self.version = module.params['version']
|
||||
self.subjectAltName = module.params['subject_alt_name']
|
||||
self.subjectAltName_critical = module.params['subject_alt_name_critical']
|
||||
self.keyUsage = module.params['key_usage']
|
||||
self.keyUsage_critical = module.params['key_usage_critical']
|
||||
self.extendedKeyUsage = module.params['extended_key_usage']
|
||||
self.extendedKeyUsage_critical = module.params['extended_key_usage_critical']
|
||||
self.basicConstraints = module.params['basic_constraints']
|
||||
self.basicConstraints_critical = module.params['basic_constraints_critical']
|
||||
self.ocspMustStaple = module.params['ocsp_must_staple']
|
||||
self.ocspMustStaple_critical = module.params['ocsp_must_staple_critical']
|
||||
self.name_constraints_permitted = module.params['name_constraints_permitted'] or []
|
||||
self.name_constraints_excluded = module.params['name_constraints_excluded'] or []
|
||||
self.name_constraints_critical = module.params['name_constraints_critical']
|
||||
self.create_subject_key_identifier = module.params['create_subject_key_identifier']
|
||||
self.subject_key_identifier = module.params['subject_key_identifier']
|
||||
self.authority_key_identifier = module.params['authority_key_identifier']
|
||||
self.authority_cert_issuer = module.params['authority_cert_issuer']
|
||||
self.authority_cert_serial_number = module.params['authority_cert_serial_number']
|
||||
self.crl_distribution_points = module.params['crl_distribution_points']
|
||||
self.csr = None
|
||||
self.privatekey = None
|
||||
|
||||
if self.create_subject_key_identifier and self.subject_key_identifier is not None:
|
||||
module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true')
|
||||
|
||||
self.subject = [
|
||||
('C', module.params['country_name']),
|
||||
('ST', module.params['state_or_province_name']),
|
||||
('L', module.params['locality_name']),
|
||||
('O', module.params['organization_name']),
|
||||
('OU', module.params['organizational_unit_name']),
|
||||
('CN', module.params['common_name']),
|
||||
('emailAddress', module.params['email_address']),
|
||||
]
|
||||
|
||||
if module.params['subject']:
|
||||
self.subject = self.subject + parse_name_field(module.params['subject'])
|
||||
self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
|
||||
|
||||
self.using_common_name_for_san = False
|
||||
if not self.subjectAltName and module.params['use_common_name_for_san']:
|
||||
for sub in self.subject:
|
||||
if sub[0] in ('commonName', 'CN'):
|
||||
self.subjectAltName = ['DNS:%s' % sub[1]]
|
||||
self.using_common_name_for_san = True
|
||||
break
|
||||
|
||||
if self.subject_key_identifier is not None:
|
||||
try:
|
||||
self.subject_key_identifier = binascii.unhexlify(self.subject_key_identifier.replace(':', ''))
|
||||
except Exception as e:
|
||||
raise CertificateSigningRequestError('Cannot parse subject_key_identifier: {0}'.format(e))
|
||||
|
||||
if self.authority_key_identifier is not None:
|
||||
try:
|
||||
self.authority_key_identifier = binascii.unhexlify(self.authority_key_identifier.replace(':', ''))
|
||||
except Exception as e:
|
||||
raise CertificateSigningRequestError('Cannot parse authority_key_identifier: {0}'.format(e))
|
||||
|
||||
self.existing_csr = None
|
||||
self.existing_csr_bytes = None
|
||||
|
||||
self.diff_before = self._get_info(None)
|
||||
self.diff_after = self._get_info(None)
|
||||
|
||||
def _get_info(self, data):
|
||||
if data is None:
|
||||
return dict()
|
||||
try:
|
||||
result = get_csr_info(
|
||||
self.module, self.backend, data, validate_signature=False, prefer_one_fingerprint=True)
|
||||
result['can_parse_csr'] = True
|
||||
return result
|
||||
except Exception as exc:
|
||||
return dict(can_parse_csr=False)
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_csr(self):
|
||||
"""(Re-)Generate CSR."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_csr_data(self):
|
||||
"""Return bytes for self.csr."""
|
||||
pass
|
||||
|
||||
def set_existing(self, csr_bytes):
|
||||
"""Set existing CSR bytes. None indicates that the CSR does not exist."""
|
||||
self.existing_csr_bytes = csr_bytes
|
||||
self.diff_after = self.diff_before = self._get_info(self.existing_csr_bytes)
|
||||
|
||||
def has_existing(self):
|
||||
"""Query whether an existing CSR is/has been there."""
|
||||
return self.existing_csr_bytes is not None
|
||||
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Load the provided private key into self.privatekey."""
|
||||
if self.privatekey is not None:
|
||||
return
|
||||
try:
|
||||
self.privatekey = load_privatekey(
|
||||
path=self.privatekey_path,
|
||||
content=self.privatekey_content,
|
||||
passphrase=self.privatekey_passphrase,
|
||||
backend=self.backend,
|
||||
)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise CertificateSigningRequestError(exc)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_csr(self):
|
||||
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
|
||||
pass
|
||||
|
||||
def needs_regeneration(self):
|
||||
"""Check whether a regeneration is necessary."""
|
||||
if self.existing_csr_bytes is None:
|
||||
return True
|
||||
try:
|
||||
self.existing_csr = load_certificate_request(None, content=self.existing_csr_bytes, backend=self.backend)
|
||||
except Exception as dummy:
|
||||
return True
|
||||
self._ensure_private_key_loaded()
|
||||
return not self._check_csr()
|
||||
|
||||
def dump(self, include_csr):
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = {
|
||||
'privatekey': self.privatekey_path,
|
||||
'subject': self.subject,
|
||||
'subjectAltName': self.subjectAltName,
|
||||
'keyUsage': self.keyUsage,
|
||||
'extendedKeyUsage': self.extendedKeyUsage,
|
||||
'basicConstraints': self.basicConstraints,
|
||||
'ocspMustStaple': self.ocspMustStaple,
|
||||
'name_constraints_permitted': self.name_constraints_permitted,
|
||||
'name_constraints_excluded': self.name_constraints_excluded,
|
||||
}
|
||||
# Get hold of CSR bytes
|
||||
csr_bytes = self.existing_csr_bytes
|
||||
if self.csr is not None:
|
||||
csr_bytes = self.get_csr_data()
|
||||
self.diff_after = self._get_info(csr_bytes)
|
||||
if include_csr:
|
||||
# Store result
|
||||
result['csr'] = csr_bytes.decode('utf-8') if csr_bytes else None
|
||||
|
||||
result['diff'] = dict(
|
||||
before=self.diff_before,
|
||||
after=self.diff_after,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class CertificateSigningRequestPyOpenSSLBackend(CertificateSigningRequestBackend):
|
||||
def __init__(self, module):
|
||||
for o in ('create_subject_key_identifier', ):
|
||||
if module.params[o]:
|
||||
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
|
||||
for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number', 'crl_distribution_points'):
|
||||
if module.params[o] is not None:
|
||||
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
|
||||
super(CertificateSigningRequestPyOpenSSLBackend, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def generate_csr(self):
|
||||
"""(Re-)Generate CSR."""
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
req = crypto.X509Req()
|
||||
req.set_version(self.version - 1)
|
||||
subject = req.get_subject()
|
||||
for entry in self.subject:
|
||||
if entry[1] is not None:
|
||||
# Workaround for https://github.com/pyca/pyopenssl/issues/165
|
||||
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(entry[0]))
|
||||
if nid == 0:
|
||||
raise CertificateSigningRequestError('Unknown subject field identifier "{0}"'.format(entry[0]))
|
||||
res = OpenSSL._util.lib.X509_NAME_add_entry_by_NID(subject._name, nid, OpenSSL._util.lib.MBSTRING_UTF8, to_bytes(entry[1]), -1, -1, 0)
|
||||
if res == 0:
|
||||
raise CertificateSigningRequestError('Invalid value for subject field identifier "{0}": {1}'.format(entry[0], entry[1]))
|
||||
|
||||
extensions = []
|
||||
if self.subjectAltName:
|
||||
altnames = ', '.join(self.subjectAltName)
|
||||
try:
|
||||
extensions.append(crypto.X509Extension(b"subjectAltName", self.subjectAltName_critical, altnames.encode('ascii')))
|
||||
except OpenSSL.crypto.Error as e:
|
||||
raise CertificateSigningRequestError(
|
||||
'Error while parsing Subject Alternative Names {0} (check for missing type prefix, such as "DNS:"!): {1}'.format(
|
||||
', '.join(["{0}".format(san) for san in self.subjectAltName]), str(e)
|
||||
)
|
||||
)
|
||||
|
||||
if self.keyUsage:
|
||||
usages = ', '.join(self.keyUsage)
|
||||
extensions.append(crypto.X509Extension(b"keyUsage", self.keyUsage_critical, usages.encode('ascii')))
|
||||
|
||||
if self.extendedKeyUsage:
|
||||
usages = ', '.join(self.extendedKeyUsage)
|
||||
extensions.append(crypto.X509Extension(b"extendedKeyUsage", self.extendedKeyUsage_critical, usages.encode('ascii')))
|
||||
|
||||
if self.basicConstraints:
|
||||
usages = ', '.join(self.basicConstraints)
|
||||
extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii')))
|
||||
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
usages = ', '.join(
|
||||
['permitted;{0}'.format(name) for name in self.name_constraints_permitted] +
|
||||
['excluded;{0}'.format(name) for name in self.name_constraints_excluded]
|
||||
)
|
||||
extensions.append(crypto.X509Extension(b"nameConstraints", self.name_constraints_critical, usages.encode('ascii')))
|
||||
|
||||
if self.ocspMustStaple:
|
||||
extensions.append(crypto.X509Extension(OPENSSL_MUST_STAPLE_NAME, self.ocspMustStaple_critical, OPENSSL_MUST_STAPLE_VALUE))
|
||||
|
||||
if extensions:
|
||||
req.add_extensions(extensions)
|
||||
|
||||
req.set_pubkey(self.privatekey)
|
||||
req.sign(self.privatekey, self.digest)
|
||||
self.csr = req
|
||||
|
||||
def get_csr_data(self):
|
||||
"""Return bytes for self.csr."""
|
||||
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.csr)
|
||||
|
||||
def _check_csr(self):
|
||||
def _check_subject(csr):
|
||||
subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in self.subject]
|
||||
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in csr.get_subject().get_components()]
|
||||
if not set(subject) == set(current_subject):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_subjectAltName(extensions):
|
||||
altnames_ext = next((ext for ext in extensions if ext.get_short_name() == b'subjectAltName'), '')
|
||||
altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(altnames_ext, errors='surrogate_or_strict').split(',') if altname.strip()]
|
||||
if self.subjectAltName:
|
||||
if (set(altnames) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.subjectAltName]) or
|
||||
altnames_ext.get_critical() != self.subjectAltName_critical):
|
||||
return False
|
||||
else:
|
||||
if altnames:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_keyUsage_(extensions, extName, expected, critical):
|
||||
usages_ext = [ext for ext in extensions if ext.get_short_name() == extName]
|
||||
if (not usages_ext and expected) or (usages_ext and not expected):
|
||||
return False
|
||||
elif not usages_ext and not expected:
|
||||
return True
|
||||
else:
|
||||
current = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage.strip())) for usage in str(usages_ext[0]).split(',')]
|
||||
expected = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage)) for usage in expected]
|
||||
return set(current) == set(expected) and usages_ext[0].get_critical() == critical
|
||||
|
||||
def _check_keyUsage(extensions):
|
||||
usages_ext = [ext for ext in extensions if ext.get_short_name() == b'keyUsage']
|
||||
if (not usages_ext and self.keyUsage) or (usages_ext and not self.keyUsage):
|
||||
return False
|
||||
elif not usages_ext and not self.keyUsage:
|
||||
return True
|
||||
else:
|
||||
# OpenSSL._util.lib.OBJ_txt2nid() always returns 0 for all keyUsage values
|
||||
# (since keyUsage has a fixed bitfield for these values and is not extensible).
|
||||
# Therefore, we create an extension for the wanted values, and compare the
|
||||
# data of the extensions (which is the serialized bitfield).
|
||||
expected_ext = crypto.X509Extension(b"keyUsage", False, ', '.join(self.keyUsage).encode('ascii'))
|
||||
return usages_ext[0].get_data() == expected_ext.get_data() and usages_ext[0].get_critical() == self.keyUsage_critical
|
||||
|
||||
def _check_extenededKeyUsage(extensions):
|
||||
return _check_keyUsage_(extensions, b'extendedKeyUsage', self.extendedKeyUsage, self.extendedKeyUsage_critical)
|
||||
|
||||
def _check_basicConstraints(extensions):
|
||||
return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical)
|
||||
|
||||
def _check_nameConstraints(extensions):
|
||||
nc_ext = next((ext for ext in extensions if ext.get_short_name() == b'nameConstraints'), '')
|
||||
permitted, excluded = pyopenssl_parse_name_constraints(nc_ext)
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
if set(permitted) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_permitted]):
|
||||
return False
|
||||
if set(excluded) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_excluded]):
|
||||
return False
|
||||
if nc_ext.get_critical() != self.name_constraints_critical:
|
||||
return False
|
||||
else:
|
||||
if permitted or excluded:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_ocspMustStaple(extensions):
|
||||
oms_ext = [ext for ext in extensions if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if self.ocspMustStaple:
|
||||
return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical
|
||||
else:
|
||||
return len(oms_ext) == 0
|
||||
|
||||
def _check_extensions(csr):
|
||||
extensions = csr.get_extensions()
|
||||
return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
|
||||
_check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
|
||||
_check_ocspMustStaple(extensions) and _check_nameConstraints(extensions))
|
||||
|
||||
def _check_signature(csr):
|
||||
try:
|
||||
return csr.verify(self.privatekey)
|
||||
except crypto.Error:
|
||||
return False
|
||||
|
||||
return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr)
|
||||
|
||||
|
||||
def parse_crl_distribution_points(module, crl_distribution_points):
|
||||
result = []
|
||||
for index, parse_crl_distribution_point in enumerate(crl_distribution_points):
|
||||
try:
|
||||
params = dict(
|
||||
full_name=None,
|
||||
relative_name=None,
|
||||
crl_issuer=None,
|
||||
reasons=None,
|
||||
)
|
||||
if parse_crl_distribution_point['full_name'] is not None:
|
||||
if not parse_crl_distribution_point['full_name']:
|
||||
raise OpenSSLObjectError('full_name must not be empty')
|
||||
params['full_name'] = [cryptography_get_name(name, 'full name') for name in parse_crl_distribution_point['full_name']]
|
||||
if parse_crl_distribution_point['relative_name'] is not None:
|
||||
if not parse_crl_distribution_point['relative_name']:
|
||||
raise OpenSSLObjectError('relative_name must not be empty')
|
||||
try:
|
||||
params['relative_name'] = cryptography_parse_relative_distinguished_name(parse_crl_distribution_point['relative_name'])
|
||||
except Exception:
|
||||
# If cryptography's version is < 1.6, the error is probably caused by that
|
||||
if CRYPTOGRAPHY_VERSION < LooseVersion('1.6'):
|
||||
raise OpenSSLObjectError('Cannot specify relative_name for cryptography < 1.6')
|
||||
raise
|
||||
if parse_crl_distribution_point['crl_issuer'] is not None:
|
||||
if not parse_crl_distribution_point['crl_issuer']:
|
||||
raise OpenSSLObjectError('crl_issuer must not be empty')
|
||||
params['crl_issuer'] = [cryptography_get_name(name, 'CRL issuer') for name in parse_crl_distribution_point['crl_issuer']]
|
||||
if parse_crl_distribution_point['reasons'] is not None:
|
||||
reasons = []
|
||||
for reason in parse_crl_distribution_point['reasons']:
|
||||
reasons.append(REVOCATION_REASON_MAP[reason])
|
||||
params['reasons'] = frozenset(reasons)
|
||||
result.append(cryptography.x509.DistributionPoint(**params))
|
||||
except (OpenSSLObjectError, ValueError) as e:
|
||||
raise OpenSSLObjectError('Error while parsing CRL distribution point #{index}: {error}'.format(index=index, error=e))
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBackend):
|
||||
def __init__(self, module):
|
||||
super(CertificateSigningRequestCryptographyBackend, self).__init__(module, 'cryptography')
|
||||
self.cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
if self.version != 1:
|
||||
module.warn('The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)')
|
||||
|
||||
if self.crl_distribution_points:
|
||||
self.crl_distribution_points = parse_crl_distribution_points(module, self.crl_distribution_points)
|
||||
|
||||
def generate_csr(self):
|
||||
"""(Re-)Generate CSR."""
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
csr = cryptography.x509.CertificateSigningRequestBuilder()
|
||||
try:
|
||||
csr = csr.subject_name(cryptography.x509.Name([
|
||||
cryptography.x509.NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject
|
||||
]))
|
||||
except ValueError as e:
|
||||
raise CertificateSigningRequestError(e)
|
||||
|
||||
if self.subjectAltName:
|
||||
csr = csr.add_extension(cryptography.x509.SubjectAlternativeName([
|
||||
cryptography_get_name(name) for name in self.subjectAltName
|
||||
]), critical=self.subjectAltName_critical)
|
||||
|
||||
if self.keyUsage:
|
||||
params = cryptography_parse_key_usage_params(self.keyUsage)
|
||||
csr = csr.add_extension(cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical)
|
||||
|
||||
if self.extendedKeyUsage:
|
||||
usages = [cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage]
|
||||
csr = csr.add_extension(cryptography.x509.ExtendedKeyUsage(usages), critical=self.extendedKeyUsage_critical)
|
||||
|
||||
if self.basicConstraints:
|
||||
params = {}
|
||||
ca, path_length = cryptography_get_basic_constraints(self.basicConstraints)
|
||||
csr = csr.add_extension(cryptography.x509.BasicConstraints(ca, path_length), critical=self.basicConstraints_critical)
|
||||
|
||||
if self.ocspMustStaple:
|
||||
try:
|
||||
# This only works with cryptography >= 2.1
|
||||
csr = csr.add_extension(cryptography.x509.TLSFeature([cryptography.x509.TLSFeatureType.status_request]), critical=self.ocspMustStaple_critical)
|
||||
except AttributeError as dummy:
|
||||
csr = csr.add_extension(
|
||||
cryptography.x509.UnrecognizedExtension(CRYPTOGRAPHY_MUST_STAPLE_NAME, CRYPTOGRAPHY_MUST_STAPLE_VALUE),
|
||||
critical=self.ocspMustStaple_critical
|
||||
)
|
||||
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
try:
|
||||
csr = csr.add_extension(cryptography.x509.NameConstraints(
|
||||
[cryptography_get_name(name, 'name constraints permitted') for name in self.name_constraints_permitted] or None,
|
||||
[cryptography_get_name(name, 'name constraints excluded') for name in self.name_constraints_excluded] or None,
|
||||
), critical=self.name_constraints_critical)
|
||||
except TypeError as e:
|
||||
raise OpenSSLObjectError('Error while parsing name constraint: {0}'.format(e))
|
||||
|
||||
if self.create_subject_key_identifier:
|
||||
csr = csr.add_extension(
|
||||
cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()),
|
||||
critical=False
|
||||
)
|
||||
elif self.subject_key_identifier is not None:
|
||||
csr = csr.add_extension(cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier), critical=False)
|
||||
|
||||
if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None:
|
||||
issuers = None
|
||||
if self.authority_cert_issuer is not None:
|
||||
issuers = [cryptography_get_name(n, 'authority cert issuer') for n in self.authority_cert_issuer]
|
||||
csr = csr.add_extension(
|
||||
cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number),
|
||||
critical=False
|
||||
)
|
||||
|
||||
if self.crl_distribution_points:
|
||||
csr = csr.add_extension(
|
||||
cryptography.x509.CRLDistributionPoints(self.crl_distribution_points),
|
||||
critical=False
|
||||
)
|
||||
|
||||
digest = None
|
||||
if cryptography_key_needs_digest_for_signing(self.privatekey):
|
||||
digest = select_message_digest(self.digest)
|
||||
if digest is None:
|
||||
raise CertificateSigningRequestError('Unsupported digest "{0}"'.format(self.digest))
|
||||
try:
|
||||
self.csr = csr.sign(self.privatekey, digest, self.cryptography_backend)
|
||||
except TypeError as e:
|
||||
if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None:
|
||||
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
|
||||
raise
|
||||
except UnicodeError as e:
|
||||
# This catches IDNAErrors, which happens when a bad name is passed as a SAN
|
||||
# (https://github.com/ansible-collections/community.crypto/issues/105).
|
||||
# For older cryptography versions, this is handled by idna, which raises
|
||||
# an idna.core.IDNAError. Later versions of cryptography deprecated and stopped
|
||||
# requiring idna, whence we cannot easily handle this error. Fortunately, in
|
||||
# most versions of idna, IDNAError extends UnicodeError. There is only version
|
||||
# 2.3 where it extends Exception instead (see
|
||||
# https://github.com/kjd/idna/commit/ebefacd3134d0f5da4745878620a6a1cba86d130
|
||||
# and then
|
||||
# https://github.com/kjd/idna/commit/ea03c7b5db7d2a99af082e0239da2b68aeea702a).
|
||||
msg = 'Error while creating CSR: {0}\n'.format(e)
|
||||
if self.using_common_name_for_san:
|
||||
self.module.fail_json(msg=msg + 'This is probably caused because the Common Name is used as a SAN.'
|
||||
' Specifying use_common_name_for_san=false might fix this.')
|
||||
self.module.fail_json(msg=msg + 'This is probably caused by an invalid Subject Alternative DNS Name.')
|
||||
|
||||
def get_csr_data(self):
|
||||
"""Return bytes for self.csr."""
|
||||
return self.csr.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM)
|
||||
|
||||
def _check_csr(self):
|
||||
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
|
||||
def _check_subject(csr):
|
||||
subject = [(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject]
|
||||
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
|
||||
return set(subject) == set(current_subject)
|
||||
|
||||
def _find_extension(extensions, exttype):
|
||||
return next(
|
||||
(ext for ext in extensions if isinstance(ext.value, exttype)),
|
||||
None
|
||||
)
|
||||
|
||||
def _check_subjectAltName(extensions):
|
||||
current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName)
|
||||
current_altnames = [to_text(altname) for altname in current_altnames_ext.value] if current_altnames_ext else []
|
||||
altnames = [to_text(cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else []
|
||||
if set(altnames) != set(current_altnames):
|
||||
return False
|
||||
if altnames:
|
||||
if current_altnames_ext.critical != self.subjectAltName_critical:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_keyUsage(extensions):
|
||||
current_keyusage_ext = _find_extension(extensions, cryptography.x509.KeyUsage)
|
||||
if not self.keyUsage:
|
||||
return current_keyusage_ext is None
|
||||
elif current_keyusage_ext is None:
|
||||
return False
|
||||
params = cryptography_parse_key_usage_params(self.keyUsage)
|
||||
for param in params:
|
||||
if getattr(current_keyusage_ext.value, '_' + param) != params[param]:
|
||||
return False
|
||||
if current_keyusage_ext.critical != self.keyUsage_critical:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_extenededKeyUsage(extensions):
|
||||
current_usages_ext = _find_extension(extensions, cryptography.x509.ExtendedKeyUsage)
|
||||
current_usages = [str(usage) for usage in current_usages_ext.value] if current_usages_ext else []
|
||||
usages = [str(cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else []
|
||||
if set(current_usages) != set(usages):
|
||||
return False
|
||||
if usages:
|
||||
if current_usages_ext.critical != self.extendedKeyUsage_critical:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_basicConstraints(extensions):
|
||||
bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints)
|
||||
current_ca = bc_ext.value.ca if bc_ext else False
|
||||
current_path_length = bc_ext.value.path_length if bc_ext else None
|
||||
ca, path_length = cryptography_get_basic_constraints(self.basicConstraints)
|
||||
# Check CA flag
|
||||
if ca != current_ca:
|
||||
return False
|
||||
# Check path length
|
||||
if path_length != current_path_length:
|
||||
return False
|
||||
# Check criticality
|
||||
if self.basicConstraints:
|
||||
return bc_ext is not None and bc_ext.critical == self.basicConstraints_critical
|
||||
else:
|
||||
return bc_ext is None
|
||||
|
||||
def _check_ocspMustStaple(extensions):
|
||||
try:
|
||||
# This only works with cryptography >= 2.1
|
||||
tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature)
|
||||
has_tlsfeature = True
|
||||
except AttributeError as dummy:
|
||||
tlsfeature_ext = next(
|
||||
(ext for ext in extensions if ext.value.oid == CRYPTOGRAPHY_MUST_STAPLE_NAME),
|
||||
None
|
||||
)
|
||||
has_tlsfeature = False
|
||||
if self.ocspMustStaple:
|
||||
if not tlsfeature_ext or tlsfeature_ext.critical != self.ocspMustStaple_critical:
|
||||
return False
|
||||
if has_tlsfeature:
|
||||
return cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||
else:
|
||||
return tlsfeature_ext.value.value == CRYPTOGRAPHY_MUST_STAPLE_VALUE
|
||||
else:
|
||||
return tlsfeature_ext is None
|
||||
|
||||
def _check_nameConstraints(extensions):
|
||||
current_nc_ext = _find_extension(extensions, cryptography.x509.NameConstraints)
|
||||
current_nc_perm = [to_text(altname) for altname in current_nc_ext.value.permitted_subtrees or []] if current_nc_ext else []
|
||||
current_nc_excl = [to_text(altname) for altname in current_nc_ext.value.excluded_subtrees or []] if current_nc_ext else []
|
||||
nc_perm = [to_text(cryptography_get_name(altname, 'name constraints permitted')) for altname in self.name_constraints_permitted]
|
||||
nc_excl = [to_text(cryptography_get_name(altname, 'name constraints excluded')) for altname in self.name_constraints_excluded]
|
||||
if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(current_nc_excl):
|
||||
return False
|
||||
if nc_perm or nc_excl:
|
||||
if current_nc_ext.critical != self.name_constraints_critical:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_subject_key_identifier(extensions):
|
||||
ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier)
|
||||
if self.create_subject_key_identifier or self.subject_key_identifier is not None:
|
||||
if not ext or ext.critical:
|
||||
return False
|
||||
if self.create_subject_key_identifier:
|
||||
digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()).digest
|
||||
return ext.value.digest == digest
|
||||
else:
|
||||
return ext.value.digest == self.subject_key_identifier
|
||||
else:
|
||||
return ext is None
|
||||
|
||||
def _check_authority_key_identifier(extensions):
|
||||
ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier)
|
||||
if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None:
|
||||
if not ext or ext.critical:
|
||||
return False
|
||||
aci = None
|
||||
csr_aci = None
|
||||
if self.authority_cert_issuer is not None:
|
||||
aci = [to_text(cryptography_get_name(n, 'authority cert issuer')) for n in self.authority_cert_issuer]
|
||||
if ext.value.authority_cert_issuer is not None:
|
||||
csr_aci = [to_text(n) for n in ext.value.authority_cert_issuer]
|
||||
return (ext.value.key_identifier == self.authority_key_identifier
|
||||
and csr_aci == aci
|
||||
and ext.value.authority_cert_serial_number == self.authority_cert_serial_number)
|
||||
else:
|
||||
return ext is None
|
||||
|
||||
def _check_crl_distribution_points(extensions):
|
||||
ext = _find_extension(extensions, cryptography.x509.CRLDistributionPoints)
|
||||
if self.crl_distribution_points is None:
|
||||
return ext is None
|
||||
if not ext:
|
||||
return False
|
||||
return list(ext.value) == self.crl_distribution_points
|
||||
|
||||
def _check_extensions(csr):
|
||||
extensions = csr.extensions
|
||||
return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
|
||||
_check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
|
||||
_check_ocspMustStaple(extensions) and _check_subject_key_identifier(extensions) and
|
||||
_check_authority_key_identifier(extensions) and _check_nameConstraints(extensions) and
|
||||
_check_crl_distribution_points(extensions))
|
||||
|
||||
def _check_signature(csr):
|
||||
if not csr.is_signature_valid:
|
||||
return False
|
||||
# To check whether public key of CSR belongs to private key,
|
||||
# encode both public keys and compare PEMs.
|
||||
key_a = csr.public_key().public_bytes(
|
||||
cryptography.hazmat.primitives.serialization.Encoding.PEM,
|
||||
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
key_b = self.privatekey.public_key().public_bytes(
|
||||
cryptography.hazmat.primitives.serialization.Encoding.PEM,
|
||||
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
return key_a == key_b
|
||||
|
||||
return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr)
|
||||
|
||||
|
||||
def select_backend(module, backend):
|
||||
if module.params['version'] != 1:
|
||||
module.deprecate('The version option will only support allowed values from community.crypto 2.0.0 on. '
|
||||
'Currently, only the value 1 is allowed by RFC 2986',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CertificateSigningRequestPyOpenSSLBackend(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, CertificateSigningRequestCryptographyBackend(module)
|
||||
else:
|
||||
raise Exception('Unsupported value for backend: {0}'.format(backend))
|
||||
|
||||
|
||||
def get_csr_argument_spec():
|
||||
return ArgumentSpec(
|
||||
argument_spec=dict(
|
||||
digest=dict(type='str', default='sha256'),
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
version=dict(type='int', default=1),
|
||||
subject=dict(type='dict'),
|
||||
country_name=dict(type='str', aliases=['C', 'countryName']),
|
||||
state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']),
|
||||
locality_name=dict(type='str', aliases=['L', 'localityName']),
|
||||
organization_name=dict(type='str', aliases=['O', 'organizationName']),
|
||||
organizational_unit_name=dict(type='str', aliases=['OU', 'organizationalUnitName']),
|
||||
common_name=dict(type='str', aliases=['CN', 'commonName']),
|
||||
email_address=dict(type='str', aliases=['E', 'emailAddress']),
|
||||
subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName']),
|
||||
subject_alt_name_critical=dict(type='bool', default=False, aliases=['subjectAltName_critical']),
|
||||
use_common_name_for_san=dict(type='bool', default=True, aliases=['useCommonNameForSAN']),
|
||||
key_usage=dict(type='list', elements='str', aliases=['keyUsage']),
|
||||
key_usage_critical=dict(type='bool', default=False, aliases=['keyUsage_critical']),
|
||||
extended_key_usage=dict(type='list', elements='str', aliases=['extKeyUsage', 'extendedKeyUsage']),
|
||||
extended_key_usage_critical=dict(type='bool', default=False, aliases=['extKeyUsage_critical', 'extendedKeyUsage_critical']),
|
||||
basic_constraints=dict(type='list', elements='str', aliases=['basicConstraints']),
|
||||
basic_constraints_critical=dict(type='bool', default=False, aliases=['basicConstraints_critical']),
|
||||
ocsp_must_staple=dict(type='bool', default=False, aliases=['ocspMustStaple']),
|
||||
ocsp_must_staple_critical=dict(type='bool', default=False, aliases=['ocspMustStaple_critical']),
|
||||
name_constraints_permitted=dict(type='list', elements='str'),
|
||||
name_constraints_excluded=dict(type='list', elements='str'),
|
||||
name_constraints_critical=dict(type='bool', default=False),
|
||||
create_subject_key_identifier=dict(type='bool', default=False),
|
||||
subject_key_identifier=dict(type='str'),
|
||||
authority_key_identifier=dict(type='str'),
|
||||
authority_cert_issuer=dict(type='list', elements='str'),
|
||||
authority_cert_serial_number=dict(type='int'),
|
||||
crl_distribution_points=dict(
|
||||
type='list',
|
||||
elements='dict',
|
||||
options=dict(
|
||||
full_name=dict(type='list', elements='str'),
|
||||
relative_name=dict(type='list', elements='str'),
|
||||
crl_issuer=dict(type='list', elements='str'),
|
||||
reasons=dict(type='list', elements='str', choices=[
|
||||
'key_compromise',
|
||||
'ca_compromise',
|
||||
'affiliation_changed',
|
||||
'superseded',
|
||||
'cessation_of_operation',
|
||||
'certificate_hold',
|
||||
'privilege_withdrawn',
|
||||
'aa_compromise',
|
||||
]),
|
||||
),
|
||||
mutually_exclusive=[('full_name', 'relative_name')],
|
||||
required_one_of=[('full_name', 'relative_name', 'crl_issuer')],
|
||||
),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
),
|
||||
required_together=[
|
||||
['authority_cert_issuer', 'authority_cert_serial_number'],
|
||||
],
|
||||
mutually_exclusive=[
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
],
|
||||
required_one_of=[
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
],
|
||||
)
|
||||
481
plugins/module_utils/crypto/module_backends/csr_info.py
Normal file
481
plugins/module_utils/crypto/module_backends/csr_info.py
Normal file
@@ -0,0 +1,481 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_certificate_request,
|
||||
get_fingerprint_of_bytes,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_decode_name,
|
||||
cryptography_get_extensions_from_csr,
|
||||
cryptography_oid_to_name,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_get_extensions_from_csr,
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_normalize_name_attribute,
|
||||
pyopenssl_parse_name_constraints,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
get_publickey_info,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CSRInfoRetrieval(object):
|
||||
def __init__(self, module, backend, content, validate_signature):
|
||||
# content must be a bytes string
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.content = content
|
||||
self.validate_signature = validate_signature
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_ordered(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_basic_constraints(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ocsp_must_staple(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_name_constraints(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key_pem(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key_object(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_authority_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_all_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _is_signature_valid(self):
|
||||
pass
|
||||
|
||||
def get_info(self, prefer_one_fingerprint=False):
|
||||
result = dict()
|
||||
self.csr = load_certificate_request(None, content=self.content, backend=self.backend)
|
||||
|
||||
subject = self._get_subject_ordered()
|
||||
result['subject'] = dict()
|
||||
for k, v in subject:
|
||||
result['subject'][k] = v
|
||||
result['subject_ordered'] = subject
|
||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
||||
(
|
||||
result['name_constraints_permitted'],
|
||||
result['name_constraints_excluded'],
|
||||
result['name_constraints_critical'],
|
||||
) = self._get_name_constraints()
|
||||
|
||||
result['public_key'] = self._get_public_key_pem()
|
||||
|
||||
public_key_info = get_publickey_info(
|
||||
self.module,
|
||||
self.backend,
|
||||
key=self._get_public_key_object(),
|
||||
prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
result.update({
|
||||
'public_key_type': public_key_info['type'],
|
||||
'public_key_data': public_key_info['public_data'],
|
||||
'public_key_fingerprints': public_key_info['fingerprints'],
|
||||
})
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
|
||||
result['extensions_by_oid'] = self._get_all_extensions()
|
||||
|
||||
result['signature_valid'] = self._is_signature_valid()
|
||||
if self.validate_signature and not result['signature_valid']:
|
||||
self.module.fail_json(
|
||||
msg='CSR signature is invalid!',
|
||||
**result
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||
"""Validate the supplied CSR, using the cryptography backend"""
|
||||
def __init__(self, module, content, validate_signature):
|
||||
super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature)
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
result = []
|
||||
for attribute in self.csr.subject:
|
||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
return result
|
||||
|
||||
def _get_key_usage(self):
|
||||
try:
|
||||
current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
|
||||
current_key_usage = current_key_ext.value
|
||||
key_usage = dict(
|
||||
digital_signature=current_key_usage.digital_signature,
|
||||
content_commitment=current_key_usage.content_commitment,
|
||||
key_encipherment=current_key_usage.key_encipherment,
|
||||
data_encipherment=current_key_usage.data_encipherment,
|
||||
key_agreement=current_key_usage.key_agreement,
|
||||
key_cert_sign=current_key_usage.key_cert_sign,
|
||||
crl_sign=current_key_usage.crl_sign,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
)
|
||||
if key_usage['key_agreement']:
|
||||
key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usage_names = dict(
|
||||
digital_signature='Digital Signature',
|
||||
content_commitment='Non Repudiation',
|
||||
key_encipherment='Key Encipherment',
|
||||
data_encipherment='Data Encipherment',
|
||||
key_agreement='Key Agreement',
|
||||
key_cert_sign='Certificate Sign',
|
||||
crl_sign='CRL Sign',
|
||||
encipher_only='Encipher Only',
|
||||
decipher_only='Decipher Only',
|
||||
)
|
||||
return sorted([
|
||||
key_usage_names[name] for name, value in key_usage.items() if value
|
||||
]), current_key_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
||||
return sorted([
|
||||
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
||||
]), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints)
|
||||
result = ['CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')]
|
||||
if ext_keyusage_ext.value.path_length is not None:
|
||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
||||
return sorted(result), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
try:
|
||||
try:
|
||||
# This only works with cryptography >= 2.1
|
||||
tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature)
|
||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||
except AttributeError:
|
||||
# Fallback for cryptography < 2.1
|
||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||
tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid)
|
||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
||||
return value, tlsfeature_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
try:
|
||||
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
||||
return result, san_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_name_constraints(self):
|
||||
try:
|
||||
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
|
||||
permitted = [cryptography_decode_name(san) for san in nc_ext.value.permitted_subtrees or []]
|
||||
excluded = [cryptography_decode_name(san) for san in nc_ext.value.excluded_subtrees or []]
|
||||
return permitted, excluded, nc_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, None, False
|
||||
|
||||
def _get_public_key_pem(self):
|
||||
return self.csr.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
|
||||
def _get_public_key_object(self):
|
||||
return self.csr.public_key()
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
try:
|
||||
ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
return ext.value.digest
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
try:
|
||||
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||
issuer = None
|
||||
if ext.value.authority_cert_issuer is not None:
|
||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, None, None
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return cryptography_get_extensions_from_csr(self.csr)
|
||||
|
||||
def _is_signature_valid(self):
|
||||
return self.csr.is_signature_valid
|
||||
|
||||
|
||||
class CSRInfoRetrievalPyOpenSSL(CSRInfoRetrieval):
|
||||
"""validate the supplied CSR."""
|
||||
|
||||
def __init__(self, module, content, validate_signature):
|
||||
super(CSRInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content, validate_signature)
|
||||
|
||||
def __get_name(self, name):
|
||||
result = []
|
||||
for sub in name.get_components():
|
||||
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
||||
return result
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
return self.__get_name(self.csr.get_subject())
|
||||
|
||||
def _get_extension(self, short_name):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == short_name:
|
||||
result = [
|
||||
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
||||
]
|
||||
return sorted(result), bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_key_usage(self):
|
||||
return self._get_extension(b'keyUsage')
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
return self._get_extension(b'extendedKeyUsage')
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
return self._get_extension(b'basicConstraints')
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
extensions = self.csr.get_extensions()
|
||||
oms_ext = [
|
||||
ext for ext in extensions
|
||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
||||
]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if oms_ext:
|
||||
return True, bool(oms_ext[0].get_critical())
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
return result, bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_name_constraints(self):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == b'nameConstraints':
|
||||
permitted, excluded = pyopenssl_parse_name_constraints(extension)
|
||||
return permitted, excluded, bool(extension.get_critical())
|
||||
return None, None, False
|
||||
|
||||
def _get_public_key_pem(self):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_PEM,
|
||||
self.csr.get_pubkey(),
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
bio = crypto._new_mem_buf()
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_public_key_object(self):
|
||||
return self.csr.get_pubkey()
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None, None, None
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return pyopenssl_get_extensions_from_csr(self.csr)
|
||||
|
||||
def _is_signature_valid(self):
|
||||
try:
|
||||
return bool(self.csr.verify(self.csr.get_pubkey()))
|
||||
except crypto.Error:
|
||||
# OpenSSL error means that key is not consistent
|
||||
return False
|
||||
|
||||
|
||||
def get_csr_info(module, backend, content, validate_signature=True, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
|
||||
elif backend == 'pyopenssl':
|
||||
info = CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
def select_backend(module, backend, content, validate_signature=True):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
621
plugins/module_utils/crypto/module_backends/privatekey.py
Normal file
621
plugins/module_utils/crypto/module_backends/privatekey.py
Normal file
@@ -0,0 +1,621 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_privatekey,
|
||||
get_fingerprint_of_privatekey,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
identify_private_key_format,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
|
||||
PrivateKeyConsistencyError,
|
||||
PrivateKeyParseError,
|
||||
get_privatekey_info,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.exceptions
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
import cryptography.hazmat.primitives.asymmetric.utils
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
class PrivateKeyError(OpenSSLObjectError):
|
||||
pass
|
||||
|
||||
|
||||
# From the object called `module`, only the following properties are used:
|
||||
#
|
||||
# - module.params[]
|
||||
# - module.warn(msg: str)
|
||||
# - module.fail_json(msg: str, **kwargs)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class PrivateKeyBackend:
|
||||
def __init__(self, module, backend):
|
||||
self.module = module
|
||||
self.type = module.params['type']
|
||||
self.size = module.params['size']
|
||||
self.curve = module.params['curve']
|
||||
self.passphrase = module.params['passphrase']
|
||||
self.cipher = module.params['cipher']
|
||||
self.format = module.params['format']
|
||||
self.format_mismatch = module.params.get('format_mismatch', 'regenerate')
|
||||
self.regenerate = module.params.get('regenerate', 'full_idempotence')
|
||||
self.backend = backend
|
||||
|
||||
self.private_key = None
|
||||
|
||||
self.existing_private_key = None
|
||||
self.existing_private_key_bytes = None
|
||||
|
||||
self.diff_before = self._get_info(None)
|
||||
self.diff_after = self._get_info(None)
|
||||
|
||||
def _get_info(self, data):
|
||||
if data is None:
|
||||
return dict()
|
||||
result = dict(can_parse_key=False)
|
||||
try:
|
||||
result.update(get_privatekey_info(
|
||||
self.module, self.backend, data, passphrase=self.passphrase,
|
||||
return_private_key_data=False, prefer_one_fingerprint=True))
|
||||
except PrivateKeyConsistencyError as exc:
|
||||
result.update(exc.result)
|
||||
except PrivateKeyParseError as exc:
|
||||
result.update(exc.result)
|
||||
except Exception as exc:
|
||||
pass
|
||||
return result
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
pass
|
||||
|
||||
def convert_private_key(self):
|
||||
"""Convert existing private key (self.existing_private_key) to new private key (self.private_key).
|
||||
|
||||
This is effectively a copy without active conversion. The conversion is done
|
||||
during load and store; get_private_key_data() uses the destination format to
|
||||
serialize the key.
|
||||
"""
|
||||
self._ensure_existing_private_key_loaded()
|
||||
self.private_key = self.existing_private_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_private_key_data(self):
|
||||
"""Return bytes for self.private_key."""
|
||||
pass
|
||||
|
||||
def set_existing(self, privatekey_bytes):
|
||||
"""Set existing private key bytes. None indicates that the key does not exist."""
|
||||
self.existing_private_key_bytes = privatekey_bytes
|
||||
self.diff_after = self.diff_before = self._get_info(self.existing_private_key_bytes)
|
||||
|
||||
def has_existing(self):
|
||||
"""Query whether an existing private key is/has been there."""
|
||||
return self.existing_private_key_bytes is not None
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_passphrase(self):
|
||||
"""Check whether provided passphrase matches, assuming self.existing_private_key_bytes has been populated."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _ensure_existing_private_key_loaded(self):
|
||||
"""Make sure that self.existing_private_key is populated from self.existing_private_key_bytes."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_size_and_type(self):
|
||||
"""Check whether provided size and type matches, assuming self.existing_private_key has been populated."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_format(self):
|
||||
"""Check whether the key file format, assuming self.existing_private_key and self.existing_private_key_bytes has been populated."""
|
||||
pass
|
||||
|
||||
def needs_regeneration(self):
|
||||
"""Check whether a regeneration is necessary."""
|
||||
if self.regenerate == 'always':
|
||||
return True
|
||||
if not self.has_existing():
|
||||
# key does not exist
|
||||
return True
|
||||
if not self._check_passphrase():
|
||||
if self.regenerate == 'full_idempotence':
|
||||
return True
|
||||
self.module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
self._ensure_existing_private_key_loaded()
|
||||
if self.regenerate != 'never':
|
||||
if not self._check_size_and_type():
|
||||
if self.regenerate in ('partial_idempotence', 'full_idempotence'):
|
||||
return True
|
||||
self.module.fail_json(msg='Key has wrong type and/or size.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
||||
# During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
|
||||
if self.format_mismatch == 'regenerate' and self.regenerate != 'never':
|
||||
if not self._check_format():
|
||||
if self.regenerate in ('partial_idempotence', 'full_idempotence'):
|
||||
return True
|
||||
self.module.fail_json(msg='Key has wrong format.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.'
|
||||
' To convert the key, set `format_mismatch` to `convert`.')
|
||||
return False
|
||||
|
||||
def needs_conversion(self):
|
||||
"""Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
|
||||
# During conversion step, convert if format does not match and format_mismatch == 'convert'
|
||||
self._ensure_existing_private_key_loaded()
|
||||
return self.has_existing() and self.format_mismatch == 'convert' and not self._check_format()
|
||||
|
||||
def _get_fingerprint(self):
|
||||
if self.private_key:
|
||||
return get_fingerprint_of_privatekey(self.private_key, backend=self.backend)
|
||||
try:
|
||||
self._ensure_existing_private_key_loaded()
|
||||
except Exception as dummy:
|
||||
# Ignore errors
|
||||
pass
|
||||
if self.existing_private_key:
|
||||
return get_fingerprint_of_privatekey(self.existing_private_key, backend=self.backend)
|
||||
|
||||
def dump(self, include_key):
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
if not self.private_key:
|
||||
try:
|
||||
self._ensure_existing_private_key_loaded()
|
||||
except Exception as dummy:
|
||||
# Ignore errors
|
||||
pass
|
||||
result = {
|
||||
'type': self.type,
|
||||
'size': self.size,
|
||||
'fingerprint': self._get_fingerprint(),
|
||||
}
|
||||
if self.type == 'ECC':
|
||||
result['curve'] = self.curve
|
||||
# Get hold of private key bytes
|
||||
pk_bytes = self.existing_private_key_bytes
|
||||
if self.private_key is not None:
|
||||
pk_bytes = self.get_private_key_data()
|
||||
self.diff_after = self._get_info(pk_bytes)
|
||||
if include_key:
|
||||
# Store result
|
||||
if pk_bytes:
|
||||
if identify_private_key_format(pk_bytes) == 'raw':
|
||||
result['privatekey'] = base64.b64encode(pk_bytes)
|
||||
else:
|
||||
result['privatekey'] = pk_bytes.decode('utf-8')
|
||||
else:
|
||||
result['privatekey'] = None
|
||||
|
||||
result['diff'] = dict(
|
||||
before=self.diff_before,
|
||||
after=self.diff_after,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class PrivateKeyPyOpenSSLBackend(PrivateKeyBackend):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyPyOpenSSLBackend, self).__init__(module=module, backend='pyopenssl')
|
||||
|
||||
if self.type == 'RSA':
|
||||
self.openssl_type = crypto.TYPE_RSA
|
||||
elif self.type == 'DSA':
|
||||
self.openssl_type = crypto.TYPE_DSA
|
||||
else:
|
||||
self.module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
|
||||
|
||||
if self.format != 'auto_ignore':
|
||||
self.module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
|
||||
|
||||
def generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
self.private_key = crypto.PKey()
|
||||
try:
|
||||
self.private_key.generate_key(self.openssl_type, self.size)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _ensure_existing_private_key_loaded(self):
|
||||
if self.existing_private_key is None and self.has_existing():
|
||||
try:
|
||||
self.existing_private_key = load_privatekey(
|
||||
None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def get_private_key_data(self):
|
||||
"""Return bytes for self.private_key"""
|
||||
if self.cipher and self.passphrase:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key,
|
||||
self.cipher, to_bytes(self.passphrase))
|
||||
else:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
load_privatekey(None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
|
||||
return True
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
return self.size == self.existing_private_key.bits() and self.openssl_type == self.existing_private_key.type()
|
||||
|
||||
def _check_format(self):
|
||||
# Not supported by this backend
|
||||
return True
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class PrivateKeyCryptographyBackend(PrivateKeyBackend):
|
||||
|
||||
def _get_ec_class(self, ectype):
|
||||
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype)
|
||||
if ecclass is None:
|
||||
self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype))
|
||||
return ecclass
|
||||
|
||||
def _add_curve(self, name, ectype, deprecated=False):
|
||||
def create(size):
|
||||
ecclass = self._get_ec_class(ectype)
|
||||
return ecclass()
|
||||
|
||||
def verify(privatekey):
|
||||
ecclass = self._get_ec_class(ectype)
|
||||
return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass)
|
||||
|
||||
self.curves[name] = {
|
||||
'create': create,
|
||||
'verify': verify,
|
||||
'deprecated': deprecated,
|
||||
}
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyCryptographyBackend, self).__init__(module=module, backend='cryptography')
|
||||
|
||||
self.curves = dict()
|
||||
self._add_curve('secp224r1', 'SECP224R1')
|
||||
self._add_curve('secp256k1', 'SECP256K1')
|
||||
self._add_curve('secp256r1', 'SECP256R1')
|
||||
self._add_curve('secp384r1', 'SECP384R1')
|
||||
self._add_curve('secp521r1', 'SECP521R1')
|
||||
self._add_curve('secp192r1', 'SECP192R1', deprecated=True)
|
||||
self._add_curve('sect163k1', 'SECT163K1', deprecated=True)
|
||||
self._add_curve('sect163r2', 'SECT163R2', deprecated=True)
|
||||
self._add_curve('sect233k1', 'SECT233K1', deprecated=True)
|
||||
self._add_curve('sect233r1', 'SECT233R1', deprecated=True)
|
||||
self._add_curve('sect283k1', 'SECT283K1', deprecated=True)
|
||||
self._add_curve('sect283r1', 'SECT283R1', deprecated=True)
|
||||
self._add_curve('sect409k1', 'SECT409K1', deprecated=True)
|
||||
self._add_curve('sect409r1', 'SECT409R1', deprecated=True)
|
||||
self._add_curve('sect571k1', 'SECT571K1', deprecated=True)
|
||||
self._add_curve('sect571r1', 'SECT571R1', deprecated=True)
|
||||
self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True)
|
||||
self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True)
|
||||
self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True)
|
||||
|
||||
self.cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
|
||||
if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X25519')
|
||||
if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X25519 serialization')
|
||||
if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X448')
|
||||
if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support Ed25519')
|
||||
if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.module.fail_json(msg='Your cryptography version does not support Ed448')
|
||||
|
||||
def _get_wanted_format(self):
|
||||
if self.format not in ('auto', 'auto_ignore'):
|
||||
return self.format
|
||||
if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'):
|
||||
return 'pkcs8'
|
||||
else:
|
||||
return 'pkcs1'
|
||||
|
||||
def generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
try:
|
||||
if self.type == 'RSA':
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
|
||||
public_exponent=65537, # OpenSSL always uses this
|
||||
key_size=self.size,
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
if self.type == 'DSA':
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
|
||||
key_size=self.size,
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
|
||||
if self.type == 'ECC' and self.curve in self.curves:
|
||||
if self.curves[self.curve]['deprecated']:
|
||||
self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve))
|
||||
self.private_key = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
|
||||
curve=self.curves[self.curve]['create'](self.size),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
|
||||
|
||||
def get_private_key_data(self):
|
||||
"""Return bytes for self.private_key"""
|
||||
# Select export format and encoding
|
||||
try:
|
||||
export_format = self._get_wanted_format()
|
||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
|
||||
if export_format == 'pkcs1':
|
||||
# "TraditionalOpenSSL" format is PKCS1
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
|
||||
elif export_format == 'pkcs8':
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
elif export_format == 'raw':
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
|
||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw
|
||||
except AttributeError:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
|
||||
|
||||
# Select key encryption
|
||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
|
||||
if self.cipher and self.passphrase:
|
||||
if self.cipher == 'auto':
|
||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase))
|
||||
else:
|
||||
self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.')
|
||||
|
||||
# Serialize key
|
||||
try:
|
||||
return self.private_key.private_bytes(
|
||||
encoding=export_encoding,
|
||||
format=export_format,
|
||||
encryption_algorithm=encryption_algorithm
|
||||
)
|
||||
except ValueError as dummy:
|
||||
self.module.fail_json(
|
||||
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
|
||||
)
|
||||
except Exception as dummy:
|
||||
self.module.fail_json(
|
||||
msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
|
||||
exception=traceback.format_exc()
|
||||
)
|
||||
|
||||
def _load_privatekey(self):
|
||||
data = self.existing_private_key_bytes
|
||||
try:
|
||||
# Interpret bytes depending on format.
|
||||
format = identify_private_key_format(data)
|
||||
if format == 'raw':
|
||||
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
|
||||
return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
|
||||
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
|
||||
return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
|
||||
if len(data) == 32:
|
||||
if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519):
|
||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519):
|
||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
||||
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
|
||||
try:
|
||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
||||
except Exception:
|
||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
||||
raise PrivateKeyError('Cannot load raw key')
|
||||
else:
|
||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
data,
|
||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except Exception as e:
|
||||
raise PrivateKeyError(e)
|
||||
|
||||
def _ensure_existing_private_key_loaded(self):
|
||||
if self.existing_private_key is None and self.has_existing():
|
||||
self.existing_private_key = self._load_privatekey()
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
format = identify_private_key_format(self.existing_private_key_bytes)
|
||||
if format == 'raw':
|
||||
# Raw keys cannot be encrypted. To avoid incompatibilities, we try to
|
||||
# actually load the key (and return False when this fails).
|
||||
self._load_privatekey()
|
||||
# Loading the key succeeded. Only return True when no passphrase was
|
||||
# provided.
|
||||
return self.passphrase is None
|
||||
else:
|
||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
self.existing_private_key_bytes,
|
||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
return self.type == 'RSA' and self.size == self.existing_private_key.key_size
|
||||
if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
return self.type == 'DSA' and self.size == self.existing_private_key.key_size
|
||||
if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
||||
return self.type == 'X25519'
|
||||
if CRYPTOGRAPHY_HAS_X448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
||||
return self.type == 'X448'
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
return self.type == 'Ed25519'
|
||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
return self.type == 'Ed448'
|
||||
if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
if self.type != 'ECC':
|
||||
return False
|
||||
if self.curve not in self.curves:
|
||||
return False
|
||||
return self.curves[self.curve]['verify'](self.existing_private_key)
|
||||
|
||||
return False
|
||||
|
||||
def _check_format(self):
|
||||
if self.format == 'auto_ignore':
|
||||
return True
|
||||
try:
|
||||
format = identify_private_key_format(self.existing_private_key_bytes)
|
||||
return format == self._get_wanted_format()
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
|
||||
def select_backend(module, backend):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
|
||||
# First try pyOpenSSL, then cryptography
|
||||
if can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
else:
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PrivateKeyPyOpenSSLBackend(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, PrivateKeyCryptographyBackend(module)
|
||||
else:
|
||||
raise Exception('Unsupported value for backend: {0}'.format(backend))
|
||||
|
||||
|
||||
def get_privatekey_argument_spec():
|
||||
return ArgumentSpec(
|
||||
argument_spec=dict(
|
||||
size=dict(type='int', default=4096),
|
||||
type=dict(type='str', default='RSA', choices=[
|
||||
'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448'
|
||||
]),
|
||||
curve=dict(type='str', choices=[
|
||||
'secp224r1', 'secp256k1', 'secp256r1', 'secp384r1', 'secp521r1',
|
||||
'secp192r1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1',
|
||||
'sect163k1', 'sect163r2', 'sect233k1', 'sect233r1', 'sect283k1',
|
||||
'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1',
|
||||
]),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
cipher=dict(type='str'),
|
||||
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
|
||||
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
regenerate=dict(
|
||||
type='str',
|
||||
default='full_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
),
|
||||
required_together=[
|
||||
['cipher', 'passphrase']
|
||||
],
|
||||
required_if=[
|
||||
['type', 'ECC', ['curve']],
|
||||
],
|
||||
)
|
||||
437
plugins/module_utils/crypto/module_backends/privatekey_info.py
Normal file
437
plugins/module_utils/crypto/module_backends/privatekey_info.py
Normal file
@@ -0,0 +1,437 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_privatekey,
|
||||
get_fingerprint_of_bytes,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
|
||||
binary_exp_mod,
|
||||
quick_is_not_prime,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
_get_cryptography_public_key_info,
|
||||
_bigint_to_int,
|
||||
_get_pyopenssl_public_key_info,
|
||||
)
|
||||
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
SIGNATURE_TEST_DATA = b'1234'
|
||||
|
||||
|
||||
def _get_cryptography_private_key_info(key):
|
||||
key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
|
||||
key_private_data = dict()
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['p'] = private_numbers.p
|
||||
key_private_data['q'] = private_numbers.q
|
||||
key_private_data['exponent'] = private_numbers.d
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['x'] = private_numbers.x
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['multiplier'] = private_numbers.private_value
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
|
||||
def _check_dsa_consistency(key_public_data, key_private_data):
|
||||
# Get parameters
|
||||
p = key_public_data.get('p')
|
||||
q = key_public_data.get('q')
|
||||
g = key_public_data.get('g')
|
||||
y = key_public_data.get('y')
|
||||
x = key_private_data.get('x')
|
||||
for v in (p, q, g, y, x):
|
||||
if v is None:
|
||||
return None
|
||||
# Make sure that g is not 0, 1 or -1 in Z/pZ
|
||||
if g < 2 or g >= p - 1:
|
||||
return False
|
||||
# Make sure that x is in range
|
||||
if x < 1 or x >= q:
|
||||
return False
|
||||
# Check whether q divides p-1
|
||||
if (p - 1) % q != 0:
|
||||
return False
|
||||
# Check that g**q mod p == 1
|
||||
if binary_exp_mod(g, q, p) != 1:
|
||||
return False
|
||||
# Check whether g**x mod p == y
|
||||
if binary_exp_mod(g, x, p) != y:
|
||||
return False
|
||||
# Check (quickly) whether p or q are not primes
|
||||
if quick_is_not_prime(q) or quick_is_not_prime(p):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
# 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:
|
||||
return result
|
||||
try:
|
||||
signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256())
|
||||
except AttributeError:
|
||||
# sign() was added in cryptography 1.5, but we support older versions
|
||||
return None
|
||||
try:
|
||||
key.public_key().verify(
|
||||
signature,
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.hashes.SHA256()
|
||||
)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
try:
|
||||
signature = key.sign(
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
||||
)
|
||||
except AttributeError:
|
||||
# sign() was added in cryptography 1.5, but we support older versions
|
||||
return None
|
||||
try:
|
||||
key.public_key().verify(
|
||||
signature,
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
||||
)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
has_simple_sign_function = False
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
has_simple_sign_function = True
|
||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
has_simple_sign_function = True
|
||||
if has_simple_sign_function:
|
||||
signature = key.sign(SIGNATURE_TEST_DATA)
|
||||
try:
|
||||
key.public_key().verify(signature, SIGNATURE_TEST_DATA)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
# For X25519 and X448, there's no test yet.
|
||||
return None
|
||||
|
||||
|
||||
class PrivateKeyConsistencyError(OpenSSLObjectError):
|
||||
def __init__(self, msg, result):
|
||||
super(PrivateKeyConsistencyError, self).__init__(msg)
|
||||
self.error_message = msg
|
||||
self.result = result
|
||||
|
||||
|
||||
class PrivateKeyParseError(OpenSSLObjectError):
|
||||
def __init__(self, msg, result):
|
||||
super(PrivateKeyParseError, self).__init__(msg)
|
||||
self.error_message = msg
|
||||
self.result = result
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class PrivateKeyInfoRetrieval(object):
|
||||
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False):
|
||||
# content must be a bytes string
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.content = content
|
||||
self.passphrase = passphrase
|
||||
self.return_private_key_data = return_private_key_data
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_info(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
pass
|
||||
|
||||
def get_info(self, prefer_one_fingerprint=False):
|
||||
result = dict(
|
||||
can_parse_key=False,
|
||||
key_is_consistent=None,
|
||||
)
|
||||
priv_key_detail = self.content
|
||||
try:
|
||||
self.key = load_privatekey(
|
||||
path=None,
|
||||
content=priv_key_detail,
|
||||
passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase,
|
||||
backend=self.backend
|
||||
)
|
||||
result['can_parse_key'] = True
|
||||
except OpenSSLObjectError as exc:
|
||||
raise PrivateKeyParseError(to_native(exc), result)
|
||||
|
||||
result['public_key'] = self._get_public_key(binary=False)
|
||||
pk = self._get_public_key(binary=True)
|
||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(
|
||||
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
|
||||
|
||||
key_type, key_public_data, key_private_data = self._get_key_info()
|
||||
result['type'] = key_type
|
||||
result['public_data'] = key_public_data
|
||||
if self.return_private_key_data:
|
||||
result['private_data'] = key_private_data
|
||||
|
||||
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
|
||||
if result['key_is_consistent'] is False:
|
||||
# Only fail when it is False, to avoid to fail on None (which means "we don't know")
|
||||
msg = (
|
||||
"Private key is not consistent! (See "
|
||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
|
||||
)
|
||||
raise PrivateKeyConsistencyError(msg, result)
|
||||
return result
|
||||
|
||||
|
||||
class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
|
||||
"""Validate the supplied private key, using the cryptography backend"""
|
||||
def __init__(self, module, content, **kwargs):
|
||||
super(PrivateKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, **kwargs)
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
return self.key.public_key().public_bytes(
|
||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
def _get_key_info(self):
|
||||
return _get_cryptography_private_key_info(self.key)
|
||||
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
|
||||
|
||||
|
||||
class PrivateKeyInfoRetrievalPyOpenSSL(PrivateKeyInfoRetrieval):
|
||||
"""validate the supplied private key."""
|
||||
|
||||
def __init__(self, module, content, **kwargs):
|
||||
super(PrivateKeyInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content, **kwargs)
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.key
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_key_info(self):
|
||||
key_type, key_public_data, try_fallback = _get_pyopenssl_public_key_info(self.key)
|
||||
key_private_data = dict()
|
||||
openssl_key_type = self.key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get modulus and exponents
|
||||
n = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
e = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
d = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
|
||||
key_private_data['exponent'] = _bigint_to_int(d[0])
|
||||
# Get factors
|
||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_factors(key, p, q)
|
||||
key_private_data['p'] = _bigint_to_int(p[0])
|
||||
key_private_data['q'] = _bigint_to_int(q[0])
|
||||
else:
|
||||
# Get private exponent
|
||||
key_private_data['exponent'] = _bigint_to_int(key.d)
|
||||
# Get factors
|
||||
key_private_data['p'] = _bigint_to_int(key.p)
|
||||
key_private_data['q'] = _bigint_to_int(key.q)
|
||||
except AttributeError:
|
||||
try_fallback = True
|
||||
elif crypto.TYPE_DSA == openssl_key_type:
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get private key exponents
|
||||
y = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
x = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_key(key, y, x)
|
||||
key_private_data['x'] = _bigint_to_int(x[0])
|
||||
else:
|
||||
# Get private key exponents
|
||||
key_private_data['x'] = _bigint_to_int(key.priv_key)
|
||||
except AttributeError:
|
||||
try_fallback = True
|
||||
else:
|
||||
# Return 'unknown'
|
||||
key_type = 'unknown ({0})'.format(self.key.type())
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _get_cryptography_private_key_info(self.key.to_cryptography_key())
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
openssl_key_type = self.key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
try:
|
||||
return self.key.check()
|
||||
except crypto.Error:
|
||||
# OpenSSL error means that key is not consistent
|
||||
return False
|
||||
if crypto.TYPE_DSA == openssl_key_type:
|
||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
||||
if result is not None:
|
||||
return result
|
||||
signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256')
|
||||
# Verify wants a cert (where it can get the public key from)
|
||||
cert = crypto.X509()
|
||||
cert.set_pubkey(self.key)
|
||||
try:
|
||||
crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256')
|
||||
return True
|
||||
except crypto.Error:
|
||||
return False
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data)
|
||||
return None
|
||||
|
||||
|
||||
def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = PrivateKeyInfoRetrievalCryptography(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
elif backend == 'pyopenssl':
|
||||
info = PrivateKeyInfoRetrievalPyOpenSSL(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PrivateKeyInfoRetrievalPyOpenSSL(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, PrivateKeyInfoRetrievalCryptography(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
320
plugins/module_utils/crypto/module_backends/publickey_info.py
Normal file
320
plugins/module_utils/crypto/module_backends/publickey_info.py
Normal file
@@ -0,0 +1,320 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2020-2021, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
get_fingerprint_of_bytes,
|
||||
load_publickey,
|
||||
)
|
||||
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '16.0.0' # when working with public key objects, the minimal required version is 0.15
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
def _get_cryptography_public_key_info(key):
|
||||
key_public_data = dict()
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
||||
key_type = 'RSA'
|
||||
public_numbers = key.public_numbers()
|
||||
key_public_data['size'] = key.key_size
|
||||
key_public_data['modulus'] = public_numbers.n
|
||||
key_public_data['exponent'] = public_numbers.e
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
|
||||
key_type = 'DSA'
|
||||
parameter_numbers = key.parameters().parameter_numbers()
|
||||
public_numbers = key.public_numbers()
|
||||
key_public_data['size'] = key.key_size
|
||||
key_public_data['p'] = parameter_numbers.p
|
||||
key_public_data['q'] = parameter_numbers.q
|
||||
key_public_data['g'] = parameter_numbers.g
|
||||
key_public_data['y'] = public_numbers.y
|
||||
elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey):
|
||||
key_type = 'X25519'
|
||||
elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey):
|
||||
key_type = 'X448'
|
||||
elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
|
||||
key_type = 'Ed25519'
|
||||
elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
|
||||
key_type = 'Ed448'
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
||||
key_type = 'ECC'
|
||||
public_numbers = key.public_numbers()
|
||||
key_public_data['curve'] = key.curve.name
|
||||
key_public_data['x'] = public_numbers.x
|
||||
key_public_data['y'] = public_numbers.y
|
||||
key_public_data['exponent_size'] = key.curve.key_size
|
||||
else:
|
||||
key_type = 'unknown ({0})'.format(type(key))
|
||||
return key_type, key_public_data
|
||||
|
||||
|
||||
def _bigint_to_int(bn):
|
||||
'''Convert OpenSSL BIGINT to Python integer'''
|
||||
if bn == OpenSSL._util.ffi.NULL:
|
||||
return None
|
||||
hexstr = OpenSSL._util.lib.BN_bn2hex(bn)
|
||||
try:
|
||||
return int(OpenSSL._util.ffi.string(hexstr), 16)
|
||||
finally:
|
||||
OpenSSL._util.lib.OPENSSL_free(hexstr)
|
||||
|
||||
|
||||
def _get_pyopenssl_public_key_info(key):
|
||||
key_public_data = dict()
|
||||
try_fallback = True
|
||||
openssl_key_type = key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
key_type = 'RSA'
|
||||
key_public_data['size'] = key.bits()
|
||||
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get modulus and exponents
|
||||
n = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
e = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
d = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
|
||||
key_public_data['modulus'] = _bigint_to_int(n[0])
|
||||
key_public_data['exponent'] = _bigint_to_int(e[0])
|
||||
else:
|
||||
# Get modulus and exponents
|
||||
key_public_data['modulus'] = _bigint_to_int(key.n)
|
||||
key_public_data['exponent'] = _bigint_to_int(key.e)
|
||||
try_fallback = False
|
||||
except AttributeError:
|
||||
# Use fallback if available
|
||||
pass
|
||||
elif crypto.TYPE_DSA == openssl_key_type:
|
||||
key_type = 'DSA'
|
||||
key_public_data['size'] = key.bits()
|
||||
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get public parameters (primes and group element)
|
||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
g = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g)
|
||||
key_public_data['p'] = _bigint_to_int(p[0])
|
||||
key_public_data['q'] = _bigint_to_int(q[0])
|
||||
key_public_data['g'] = _bigint_to_int(g[0])
|
||||
# Get public key exponents
|
||||
y = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
x = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_key(key, y, x)
|
||||
key_public_data['y'] = _bigint_to_int(y[0])
|
||||
else:
|
||||
# Get public parameters (primes and group element)
|
||||
key_public_data['p'] = _bigint_to_int(key.p)
|
||||
key_public_data['q'] = _bigint_to_int(key.q)
|
||||
key_public_data['g'] = _bigint_to_int(key.g)
|
||||
# Get public key exponents
|
||||
key_public_data['y'] = _bigint_to_int(key.pub_key)
|
||||
try_fallback = False
|
||||
except AttributeError:
|
||||
# Use fallback if available
|
||||
pass
|
||||
else:
|
||||
# Return 'unknown'
|
||||
key_type = 'unknown ({0})'.format(key.type())
|
||||
return key_type, key_public_data, try_fallback
|
||||
|
||||
|
||||
class PublicKeyParseError(OpenSSLObjectError):
|
||||
def __init__(self, msg, result):
|
||||
super(PublicKeyParseError, self).__init__(msg)
|
||||
self.error_message = msg
|
||||
self.result = result
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class PublicKeyInfoRetrieval(object):
|
||||
def __init__(self, module, backend, content=None, key=None):
|
||||
# content must be a bytes string
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.content = content
|
||||
self.key = key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_info(self):
|
||||
pass
|
||||
|
||||
def get_info(self, prefer_one_fingerprint=False):
|
||||
result = dict()
|
||||
if self.key is None:
|
||||
try:
|
||||
self.key = load_publickey(content=self.content, backend=self.backend)
|
||||
except OpenSSLObjectError as e:
|
||||
raise PublicKeyParseError(to_native(e), {})
|
||||
|
||||
pk = self._get_public_key(binary=True)
|
||||
result['fingerprints'] = get_fingerprint_of_bytes(
|
||||
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
|
||||
|
||||
key_type, key_public_data = self._get_key_info()
|
||||
result['type'] = key_type
|
||||
result['public_data'] = key_public_data
|
||||
return result
|
||||
|
||||
|
||||
class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval):
|
||||
"""Validate the supplied public key, using the cryptography backend"""
|
||||
def __init__(self, module, content=None, key=None):
|
||||
super(PublicKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content=content, key=key)
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
return self.key.public_bytes(
|
||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
def _get_key_info(self):
|
||||
return _get_cryptography_public_key_info(self.key)
|
||||
|
||||
|
||||
class PublicKeyInfoRetrievalPyOpenSSL(PublicKeyInfoRetrieval):
|
||||
"""validate the supplied public key."""
|
||||
|
||||
def __init__(self, module, content=None, key=None):
|
||||
super(PublicKeyInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content=content, key=key)
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.key
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_key_info(self):
|
||||
key_type, key_public_data, try_fallback = _get_pyopenssl_public_key_info(self.key)
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _get_cryptography_public_key_info(self.key.to_cryptography_key())
|
||||
return key_type, key_public_data
|
||||
|
||||
|
||||
def get_publickey_info(module, backend, content=None, key=None, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
|
||||
elif backend == 'pyopenssl':
|
||||
info = PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
def select_backend(module, backend, content=None, key=None):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
24
plugins/module_utils/crypto/openssh.py
Normal file
24
plugins/module_utils/crypto/openssh.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
# This import is only to maintain backwards compatibility
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
parse_openssh_version
|
||||
)
|
||||
@@ -54,3 +54,31 @@ def identify_private_key_format(content):
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return 'raw'
|
||||
|
||||
|
||||
def split_pem_list(text, keep_inbetween=False):
|
||||
'''
|
||||
Split concatenated PEM objects into a list of strings, where each is one PEM object.
|
||||
'''
|
||||
result = []
|
||||
current = [] if keep_inbetween else None
|
||||
for line in text.splitlines(True):
|
||||
if line.strip():
|
||||
if not keep_inbetween and line.startswith('-----BEGIN '):
|
||||
current = []
|
||||
if current is not None:
|
||||
current.append(line)
|
||||
if line.startswith('-----END '):
|
||||
result.append(''.join(current))
|
||||
current = [] if keep_inbetween else None
|
||||
return result
|
||||
|
||||
|
||||
def extract_first_pem(text):
|
||||
'''
|
||||
Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none.
|
||||
'''
|
||||
all_pems = split_pem_list(text)
|
||||
if not all_pems:
|
||||
return None
|
||||
return all_pems[0]
|
||||
@@ -21,13 +21,15 @@ __metaclass__ = type
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
from ._objects import OID_LOOKUP
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
@@ -38,6 +40,10 @@ from ._objects import (
|
||||
|
||||
from ._obj2txt import obj2txt
|
||||
|
||||
from .basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
|
||||
def pyopenssl_normalize_name(name, short=False):
|
||||
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name))
|
||||
@@ -56,9 +62,13 @@ def pyopenssl_normalize_name_attribute(san):
|
||||
if san.startswith('IP Address:'):
|
||||
san = 'IP:' + san[len('IP Address:'):]
|
||||
if san.startswith('IP:'):
|
||||
ip = compat_ipaddress.ip_address(san[3:])
|
||||
san = 'IP:{0}'.format(ip.compressed)
|
||||
|
||||
address = san[3:]
|
||||
if '/' in address:
|
||||
ip = compat_ipaddress.ip_network(address)
|
||||
san = 'IP:{0}/{1}'.format(ip.network_address.compressed, ip.prefixlen)
|
||||
else:
|
||||
ip = compat_ipaddress.ip_address(address)
|
||||
san = 'IP:{0}'.format(ip.compressed)
|
||||
if san.startswith('Registered ID:'):
|
||||
san = 'RID:' + san[len('Registered ID:'):]
|
||||
# Some versions of OpenSSL apparently forgot the colon. Happens in CI with Ubuntu 16.04 and FreeBSD 11.1
|
||||
@@ -79,18 +89,25 @@ def pyopenssl_get_extensions_from_cert(cert):
|
||||
critical=bool(ext.get_critical()),
|
||||
value=base64.b64encode(ext.get_data()),
|
||||
)
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
try:
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
except AttributeError:
|
||||
# When PyOpenSSL is used with cryptography >= 35.0.0, obj2txt cannot be used.
|
||||
# We try to figure out the OID with our internal lookup table, and if we fail,
|
||||
# we use the short name OpenSSL returns.
|
||||
oid = to_native(ext.get_short_name())
|
||||
oid = OID_LOOKUP.get(oid, oid)
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
@@ -105,17 +122,49 @@ def pyopenssl_get_extensions_from_csr(csr):
|
||||
critical=bool(ext.get_critical()),
|
||||
value=base64.b64encode(ext.get_data()),
|
||||
)
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
try:
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
except AttributeError:
|
||||
# When PyOpenSSL is used with cryptography >= 35.0.0, obj2txt cannot be used.
|
||||
# We try to figure out the OID with our internal lookup table, and if we fail,
|
||||
# we use the short name OpenSSL returns.
|
||||
oid = to_native(ext.get_short_name())
|
||||
oid = OID_LOOKUP.get(oid, oid)
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def pyopenssl_parse_name_constraints(name_constraints_extension):
|
||||
lines = to_text(name_constraints_extension, errors='surrogate_or_strict').splitlines()
|
||||
exclude = None
|
||||
excluded = []
|
||||
permitted = []
|
||||
for line in lines:
|
||||
if line.startswith(' ') or line.startswith('\t'):
|
||||
name = pyopenssl_normalize_name_attribute(line.strip())
|
||||
if exclude is True:
|
||||
excluded.append(name)
|
||||
elif exclude is False:
|
||||
permitted.append(name)
|
||||
else:
|
||||
raise OpenSSLObjectError('Unexpected nameConstraint line: "{0}"'.format(line))
|
||||
else:
|
||||
line_lc = line.lower()
|
||||
if line_lc.startswith('exclud'):
|
||||
exclude = True
|
||||
elif line_lc.startswith('includ') or line_lc.startswith('permitt'):
|
||||
exclude = False
|
||||
else:
|
||||
raise OpenSSLObjectError('Cannot parse nameConstraint line: "{0}"'.format(line))
|
||||
return permitted, excluded
|
||||
|
||||
@@ -27,12 +27,12 @@ import os
|
||||
import re
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
@@ -52,7 +52,14 @@ from .basic import (
|
||||
)
|
||||
|
||||
|
||||
def get_fingerprint_of_bytes(source):
|
||||
# This list of preferred fingerprints is used when prefer_one=True is supplied to the
|
||||
# fingerprinting methods.
|
||||
PREFERRED_FINGERPRINTS = (
|
||||
'sha256', 'sha3_256', 'sha512', 'sha3_512', 'sha384', 'sha3_384', 'sha1', 'md5'
|
||||
)
|
||||
|
||||
|
||||
def get_fingerprint_of_bytes(source, prefer_one=False):
|
||||
"""Generate the fingerprint of the given bytes."""
|
||||
|
||||
fingerprint = {}
|
||||
@@ -65,6 +72,12 @@ def get_fingerprint_of_bytes(source):
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
if prefer_one:
|
||||
# Sort algorithms to have the ones in PREFERRED_FINGERPRINTS at the beginning
|
||||
prefered_algorithms = [algorithm for algorithm in PREFERRED_FINGERPRINTS if algorithm in algorithms]
|
||||
prefered_algorithms += sorted([algorithm for algorithm in algorithms if algorithm not in PREFERRED_FINGERPRINTS])
|
||||
algorithms = prefered_algorithms
|
||||
|
||||
for algo in algorithms:
|
||||
f = getattr(hashlib, algo)
|
||||
try:
|
||||
@@ -79,15 +92,15 @@ def get_fingerprint_of_bytes(source):
|
||||
except TypeError:
|
||||
pubkey_digest = h.hexdigest(32)
|
||||
fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2))
|
||||
if prefer_one:
|
||||
break
|
||||
|
||||
return fingerprint
|
||||
|
||||
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
||||
def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl', prefer_one=False):
|
||||
"""Generate the fingerprint of the public key. """
|
||||
|
||||
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
try:
|
||||
publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey)
|
||||
@@ -109,7 +122,15 @@ def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
return get_fingerprint_of_bytes(publickey)
|
||||
return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one)
|
||||
|
||||
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl', prefer_one=False):
|
||||
"""Generate the fingerprint of the public key. """
|
||||
|
||||
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
||||
|
||||
return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one)
|
||||
|
||||
|
||||
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
|
||||
@@ -125,57 +146,79 @@ def load_privatekey(path, passphrase=None, check_passphrase=True, content=None,
|
||||
priv_key_detail = b_priv_key_fh.read()
|
||||
else:
|
||||
priv_key_detail = content
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
|
||||
# First try: try to load with real passphrase (resp. empty string)
|
||||
# Will work if this is the correct passphrase, or the key is not
|
||||
# password-protected.
|
||||
try:
|
||||
result = crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
priv_key_detail,
|
||||
to_bytes(passphrase or ''))
|
||||
except crypto.Error as e:
|
||||
if len(e.args) > 0 and len(e.args[0]) > 0:
|
||||
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
|
||||
# This happens in case we have the wrong passphrase.
|
||||
if passphrase is not None:
|
||||
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!')
|
||||
else:
|
||||
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
if check_passphrase:
|
||||
# Next we want to make sure that the key is actually protected by
|
||||
# a passphrase (in case we did try the empty string before, make
|
||||
# sure that the key is not protected by the empty string)
|
||||
try:
|
||||
crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
priv_key_detail,
|
||||
to_bytes('y' if passphrase == 'x' else 'x'))
|
||||
if passphrase is not None:
|
||||
# Since we can load the key without an exception, the
|
||||
# key isn't password-protected
|
||||
raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!')
|
||||
except crypto.Error as e:
|
||||
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0:
|
||||
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
|
||||
# The key is obviously protected by the empty string.
|
||||
# Don't do this at home (if it's possible at all)...
|
||||
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
|
||||
elif backend == 'cryptography':
|
||||
try:
|
||||
result = load_pem_private_key(priv_key_detail,
|
||||
None if passphrase is None else to_bytes(passphrase),
|
||||
cryptography_backend())
|
||||
except TypeError:
|
||||
raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key')
|
||||
except ValueError:
|
||||
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key')
|
||||
|
||||
return result
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
|
||||
# First try: try to load with real passphrase (resp. empty string)
|
||||
# Will work if this is the correct passphrase, or the key is not
|
||||
# password-protected.
|
||||
try:
|
||||
result = crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
priv_key_detail,
|
||||
to_bytes(passphrase or ''))
|
||||
except crypto.Error as e:
|
||||
if len(e.args) > 0 and len(e.args[0]) > 0:
|
||||
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
|
||||
# This happens in case we have the wrong passphrase.
|
||||
if passphrase is not None:
|
||||
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!')
|
||||
else:
|
||||
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
if check_passphrase:
|
||||
# Next we want to make sure that the key is actually protected by
|
||||
# a passphrase (in case we did try the empty string before, make
|
||||
# sure that the key is not protected by the empty string)
|
||||
try:
|
||||
crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
priv_key_detail,
|
||||
to_bytes('y' if passphrase == 'x' else 'x'))
|
||||
if passphrase is not None:
|
||||
# Since we can load the key without an exception, the
|
||||
# key isn't password-protected
|
||||
raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!')
|
||||
except crypto.Error as e:
|
||||
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0:
|
||||
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
|
||||
# The key is obviously protected by the empty string.
|
||||
# Don't do this at home (if it's possible at all)...
|
||||
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
|
||||
elif backend == 'cryptography':
|
||||
try:
|
||||
result = load_pem_private_key(priv_key_detail,
|
||||
None if passphrase is None else to_bytes(passphrase),
|
||||
cryptography_backend())
|
||||
except TypeError:
|
||||
raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key')
|
||||
except ValueError:
|
||||
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key')
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def load_publickey(path=None, content=None, backend=None):
|
||||
if content is None:
|
||||
if path is None:
|
||||
raise OpenSSLObjectError('Must provide either path or content')
|
||||
try:
|
||||
with open(path, 'rb') as b_priv_key_fh:
|
||||
content = b_priv_key_fh.read()
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
if backend == 'cryptography':
|
||||
try:
|
||||
return serialization.load_pem_public_key(content, backend=cryptography_backend())
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
else:
|
||||
try:
|
||||
return crypto.load_publickey(crypto.FILETYPE_PEM, content)
|
||||
except crypto.Error as e:
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
|
||||
|
||||
def load_certificate(path, content=None, backend='pyopenssl'):
|
||||
"""Load the specified certificate."""
|
||||
@@ -186,12 +229,15 @@ def load_certificate(path, content=None, backend='pyopenssl'):
|
||||
cert_content = cert_fh.read()
|
||||
else:
|
||||
cert_content = content
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
|
||||
elif backend == 'cryptography':
|
||||
return x509.load_pem_x509_certificate(cert_content, cryptography_backend())
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
|
||||
elif backend == 'cryptography':
|
||||
try:
|
||||
return x509.load_pem_x509_certificate(cert_content, cryptography_backend())
|
||||
except ValueError as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
@@ -207,7 +253,10 @@ def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content)
|
||||
elif backend == 'cryptography':
|
||||
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
|
||||
try:
|
||||
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
|
||||
except ValueError as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def parse_name_field(input_dict):
|
||||
@@ -322,6 +371,8 @@ class OpenSSLObject(object):
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||
return False
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
if not perms_required:
|
||||
@@ -343,6 +394,10 @@ class OpenSSLObject(object):
|
||||
|
||||
def remove(self, module):
|
||||
"""Remove the resource from the filesystem."""
|
||||
if self.check_mode:
|
||||
if os.path.exists(self.path):
|
||||
self.changed = True
|
||||
return
|
||||
|
||||
try:
|
||||
os.remove(self.path)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# their own license to the complete work.
|
||||
#
|
||||
# Copyright (c), Entrust Datacard Corporation, 2019
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
@@ -37,7 +37,7 @@ import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_text, to_native
|
||||
from ansible.module_utils.common.text.converters import to_text, to_native
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
|
||||
@@ -92,7 +92,8 @@ def write_file(module, content, default_mode=None, path=None):
|
||||
# Move tempfile to final destination
|
||||
module.atomic_move(tmp_name, file_args['path'])
|
||||
# Try to update permissions again
|
||||
module.set_fs_attributes_if_different(file_args, False)
|
||||
if not module.check_file_absent_if_check_mode(file_args['path']):
|
||||
module.set_fs_attributes_if_different(file_args, False)
|
||||
except Exception as e:
|
||||
try:
|
||||
os.remove(tmp_name)
|
||||
|
||||
337
plugins/module_utils/openssh/backends/common.py
Normal file
337
plugins/module_utils/openssh/backends/common.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import abc
|
||||
import os
|
||||
import stat
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
parse_openssh_version,
|
||||
)
|
||||
|
||||
|
||||
def restore_on_failure(f):
|
||||
def backup_and_restore(module, path, *args, **kwargs):
|
||||
backup_file = module.backup_local(path) if os.path.exists(path) else None
|
||||
|
||||
try:
|
||||
f(module, path, *args, **kwargs)
|
||||
except Exception:
|
||||
if backup_file is not None:
|
||||
module.atomic_move(backup_file, path)
|
||||
raise
|
||||
else:
|
||||
module.add_cleanup_file(backup_file)
|
||||
|
||||
return backup_and_restore
|
||||
|
||||
|
||||
@restore_on_failure
|
||||
def safe_atomic_move(module, path, destination):
|
||||
module.atomic_move(path, destination)
|
||||
|
||||
|
||||
def _restore_all_on_failure(f):
|
||||
def backup_and_restore(self, sources_and_destinations, *args, **kwargs):
|
||||
backups = [(d, self.module.backup_local(d)) for s, d in sources_and_destinations if os.path.exists(d)]
|
||||
|
||||
try:
|
||||
f(self, sources_and_destinations, *args, **kwargs)
|
||||
except Exception:
|
||||
for destination, backup in backups:
|
||||
self.module.atomic_move(backup, destination)
|
||||
raise
|
||||
else:
|
||||
for destination, backup in backups:
|
||||
self.module.add_cleanup_file(backup)
|
||||
return backup_and_restore
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OpensshModule(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.changed = False
|
||||
self.check_mode = self.module.check_mode
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
self._execute()
|
||||
except Exception as e:
|
||||
self.module.fail_json(
|
||||
msg="unexpected error occurred: %s" % to_native(e),
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
self.module.exit_json(**self.result)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _execute(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
result = self._result
|
||||
|
||||
result['changed'] = self.changed
|
||||
|
||||
if self.module._diff:
|
||||
result['diff'] = self.diff
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def _result(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def diff(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def skip_if_check_mode(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not self.check_mode:
|
||||
f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def trigger_change(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
f(self, *args, **kwargs)
|
||||
self.changed = True
|
||||
return wrapper
|
||||
|
||||
def _check_if_base_dir(self, path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
self.module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
def _get_ssh_version(self):
|
||||
ssh_bin = self.module.get_bin_path('ssh')
|
||||
if not ssh_bin:
|
||||
return ""
|
||||
return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'])[2].strip())
|
||||
|
||||
@_restore_all_on_failure
|
||||
def _safe_secure_move(self, sources_and_destinations):
|
||||
"""Moves a list of files from 'source' to 'destination' and restores 'destination' from backup upon failure.
|
||||
If 'destination' does not already exist, then 'source' permissions are preserved to prevent
|
||||
exposing protected data ('atomic_move' uses the 'destination' base directory mask for
|
||||
permissions if 'destination' does not already exists).
|
||||
"""
|
||||
for source, destination in sources_and_destinations:
|
||||
if os.path.exists(destination):
|
||||
self.module.atomic_move(source, destination)
|
||||
else:
|
||||
self.module.preserved_copy(source, destination)
|
||||
|
||||
def _update_permissions(self, path):
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
file_args['path'] = path
|
||||
|
||||
if not self.module.check_file_absent_if_check_mode(path):
|
||||
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
else:
|
||||
self.changed = True
|
||||
|
||||
|
||||
class KeygenCommand(object):
|
||||
def __init__(self, module):
|
||||
self._bin_path = module.get_bin_path('ssh-keygen', True)
|
||||
self._run_command = module.run_command
|
||||
|
||||
def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals,
|
||||
serial_number, signature_algorithm, signing_key_path, type,
|
||||
time_parameters, use_agent, **kwargs):
|
||||
args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier]
|
||||
|
||||
if options:
|
||||
for option in options:
|
||||
args.extend(['-O', option])
|
||||
if pkcs11_provider:
|
||||
args.extend(['-D', pkcs11_provider])
|
||||
if principals:
|
||||
args.extend(['-n', ','.join(principals)])
|
||||
if serial_number is not None:
|
||||
args.extend(['-z', str(serial_number)])
|
||||
if type == 'host':
|
||||
args.extend(['-h'])
|
||||
if use_agent:
|
||||
args.extend(['-U'])
|
||||
if time_parameters.validity_string:
|
||||
args.extend(['-V', time_parameters.validity_string])
|
||||
if signature_algorithm:
|
||||
args.extend(['-t', signature_algorithm])
|
||||
args.append(certificate_path)
|
||||
|
||||
return self._run_command(args, **kwargs)
|
||||
|
||||
def generate_keypair(self, private_key_path, size, type, comment, **kwargs):
|
||||
args = [
|
||||
self._bin_path,
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(size),
|
||||
'-t', type,
|
||||
'-f', private_key_path,
|
||||
'-C', comment or ''
|
||||
]
|
||||
|
||||
# "y" must be entered in response to the "overwrite" prompt
|
||||
data = 'y' if os.path.exists(private_key_path) else None
|
||||
|
||||
return self._run_command(args, data=data, **kwargs)
|
||||
|
||||
def get_certificate_info(self, certificate_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-L', '-f', certificate_path], **kwargs)
|
||||
|
||||
def get_matching_public_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-P', '', '-y', '-f', private_key_path], **kwargs)
|
||||
|
||||
def get_private_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-l', '-f', private_key_path], **kwargs)
|
||||
|
||||
def update_comment(self, private_key_path, comment, **kwargs):
|
||||
if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK):
|
||||
try:
|
||||
os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
except (IOError, OSError) as e:
|
||||
raise e("The private key at %s is not writeable preventing a comment update" % private_key_path)
|
||||
|
||||
return self._run_command([self._bin_path, '-q', '-o', '-c', '-C', comment, '-f', private_key_path], **kwargs)
|
||||
|
||||
|
||||
class PrivateKey(object):
|
||||
def __init__(self, size, key_type, fingerprint):
|
||||
self._size = size
|
||||
self._type = key_type
|
||||
self._fingerprint = fingerprint
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def fingerprint(self):
|
||||
return self._fingerprint
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
properties = string.split()
|
||||
|
||||
return cls(
|
||||
size=int(properties[0]),
|
||||
key_type=properties[-1][1:-1].lower(),
|
||||
fingerprint=properties[1],
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'size': self._size,
|
||||
'type': self._type,
|
||||
'fingerprint': self._fingerprint,
|
||||
}
|
||||
|
||||
|
||||
class PublicKey(object):
|
||||
def __init__(self, type_string, data, comment):
|
||||
self._type_string = type_string
|
||||
self._data = data
|
||||
self._comment = comment
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
|
||||
return all([
|
||||
self._type_string == other._type_string,
|
||||
self._data == other._data,
|
||||
(self._comment == other._comment) if self._comment is not None and other._comment is not None else True
|
||||
])
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self._type_string, self._data)
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
return self._comment
|
||||
|
||||
@comment.setter
|
||||
def comment(self, value):
|
||||
self._comment = value
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def type_string(self):
|
||||
return self._type_string
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
properties = string.strip('\n').split(' ', 2)
|
||||
|
||||
return cls(
|
||||
type_string=properties[0],
|
||||
data=properties[1],
|
||||
comment=properties[2] if len(properties) > 2 else ""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
properties = f.read().strip(' \n').split(' ', 2)
|
||||
except (IOError, OSError):
|
||||
raise
|
||||
|
||||
if len(properties) < 2:
|
||||
return None
|
||||
|
||||
return cls(
|
||||
type_string=properties[0],
|
||||
data=properties[1],
|
||||
comment='' if len(properties) <= 2 else properties[2],
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'comment': self._comment,
|
||||
'public_key': self._data,
|
||||
}
|
||||
467
plugins/module_utils/openssh/backends/keypair_backend.py
Normal file
467
plugins/module_utils/openssh/backends/keypair_backend.py
Normal file
@@ -0,0 +1,467 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import abc
|
||||
import os
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import (
|
||||
HAS_OPENSSH_SUPPORT,
|
||||
HAS_OPENSSH_PRIVATE_FORMAT,
|
||||
InvalidCommentError,
|
||||
InvalidPassphraseError,
|
||||
InvalidPrivateKeyFileError,
|
||||
OpenSSHError,
|
||||
OpensshKeypair,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
|
||||
KeygenCommand,
|
||||
OpensshModule,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
any_in,
|
||||
file_mode,
|
||||
secure_write,
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class KeypairBackend(OpensshModule):
|
||||
|
||||
def __init__(self, module):
|
||||
super(KeypairBackend, self).__init__(module)
|
||||
|
||||
self.comment = self.module.params['comment']
|
||||
self.private_key_path = self.module.params['path']
|
||||
self.public_key_path = self.private_key_path + '.pub'
|
||||
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||
self.state = self.module.params['state']
|
||||
self.type = self.module.params['type']
|
||||
|
||||
self.size = self._get_size(self.module.params['size'])
|
||||
self._validate_path()
|
||||
|
||||
self.original_private_key = None
|
||||
self.original_public_key = None
|
||||
self.private_key = None
|
||||
self.public_key = None
|
||||
|
||||
def _get_size(self, size):
|
||||
if self.type in ('rsa', 'rsa1'):
|
||||
result = 4096 if size is None else size
|
||||
if result < 1024:
|
||||
return self.module.fail_json(
|
||||
msg="For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. " +
|
||||
"Attempting to use bit lengths under 1024 will cause the module to fail."
|
||||
)
|
||||
elif self.type == 'dsa':
|
||||
result = 1024 if size is None else size
|
||||
if result != 1024:
|
||||
return self.module.fail_json(msg="DSA keys must be exactly 1024 bits as specified by FIPS 186-2.")
|
||||
elif self.type == 'ecdsa':
|
||||
result = 256 if size is None else size
|
||||
if result not in (256, 384, 521):
|
||||
return self.module.fail_json(
|
||||
msg="For ECDSA keys, size determines the key length by selecting from one of " +
|
||||
"three elliptic curve sizes: 256, 384 or 521 bits. " +
|
||||
"Attempting to use bit lengths other than these three values for ECDSA keys will " +
|
||||
"cause this module to fail."
|
||||
)
|
||||
elif self.type == 'ed25519':
|
||||
# User input is ignored for `key size` when `key type` is ed25519
|
||||
result = 256
|
||||
else:
|
||||
return self.module.fail_json(msg="%s is not a valid value for key type" % self.type)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_path(self):
|
||||
self._check_if_base_dir(self.private_key_path)
|
||||
|
||||
if os.path.isdir(self.private_key_path):
|
||||
self.module.fail_json(msg='%s is a directory. Please specify a path to a file.' % self.private_key_path)
|
||||
|
||||
def _execute(self):
|
||||
self.original_private_key = self._load_private_key()
|
||||
self.original_public_key = self._load_public_key()
|
||||
|
||||
if self.state == 'present':
|
||||
self._validate_key_load()
|
||||
|
||||
if self._should_generate():
|
||||
self._generate()
|
||||
elif not self._public_key_valid():
|
||||
self._restore_public_key()
|
||||
|
||||
self.private_key = self._load_private_key()
|
||||
self.public_key = self._load_public_key()
|
||||
|
||||
for path in (self.private_key_path, self.public_key_path):
|
||||
self._update_permissions(path)
|
||||
else:
|
||||
if self._should_remove():
|
||||
self._remove()
|
||||
|
||||
def _load_private_key(self):
|
||||
result = None
|
||||
if self._private_key_exists():
|
||||
try:
|
||||
result = self._get_private_key()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def _private_key_exists(self):
|
||||
return os.path.exists(self.private_key_path)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_private_key(self):
|
||||
pass
|
||||
|
||||
def _load_public_key(self):
|
||||
result = None
|
||||
if self._public_key_exists():
|
||||
try:
|
||||
result = PublicKey.load(self.public_key_path)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return result
|
||||
|
||||
def _public_key_exists(self):
|
||||
return os.path.exists(self.public_key_path)
|
||||
|
||||
def _validate_key_load(self):
|
||||
if (self._private_key_exists()
|
||||
and self.regenerate in ('never', 'fail', 'partial_idempotence')
|
||||
and (self.original_private_key is None or not self._private_key_readable())):
|
||||
self.module.fail_json(
|
||||
msg="Unable to read the key. The key is protected with a passphrase or broken. " +
|
||||
"Will not proceed. To force regeneration, call the module with `generate` " +
|
||||
"set to `full_idempotence` or `always`, or with `force=yes`."
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _private_key_readable(self):
|
||||
pass
|
||||
|
||||
def _should_generate(self):
|
||||
if self.original_private_key is None:
|
||||
return True
|
||||
elif self.regenerate == 'never':
|
||||
return False
|
||||
elif self.regenerate == 'fail':
|
||||
if not self._private_key_valid():
|
||||
self.module.fail_json(
|
||||
msg="Key has wrong type and/or size. Will not proceed. " +
|
||||
"To force regeneration, call the module with `generate` set to " +
|
||||
"`partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`."
|
||||
)
|
||||
return False
|
||||
elif self.regenerate in ('partial_idempotence', 'full_idempotence'):
|
||||
return not self._private_key_valid()
|
||||
else:
|
||||
return True
|
||||
|
||||
def _private_key_valid(self):
|
||||
if self.original_private_key is None:
|
||||
return False
|
||||
|
||||
return all([
|
||||
self.size == self.original_private_key.size,
|
||||
self.type == self.original_private_key.type,
|
||||
])
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _generate(self):
|
||||
temp_private_key, temp_public_key = self._generate_temp_keypair()
|
||||
|
||||
try:
|
||||
self._safe_secure_move([(temp_private_key, self.private_key_path), (temp_public_key, self.public_key_path)])
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
def _generate_temp_keypair(self):
|
||||
temp_private_key = os.path.join(self.module.tmpdir, os.path.basename(self.private_key_path))
|
||||
temp_public_key = temp_private_key + '.pub'
|
||||
|
||||
try:
|
||||
self._generate_keypair(temp_private_key)
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
for f in (temp_private_key, temp_public_key):
|
||||
self.module.add_cleanup_file(f)
|
||||
|
||||
return temp_private_key, temp_public_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _generate_keypair(self, private_key_path):
|
||||
pass
|
||||
|
||||
def _public_key_valid(self):
|
||||
if self.original_public_key is None:
|
||||
return False
|
||||
|
||||
valid_public_key = self._get_public_key()
|
||||
valid_public_key.comment = self.comment
|
||||
|
||||
return self.original_public_key == valid_public_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self):
|
||||
pass
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _restore_public_key(self):
|
||||
try:
|
||||
temp_public_key = self._create_temp_public_key(str(self._get_public_key()) + '\n')
|
||||
self._safe_secure_move([
|
||||
(temp_public_key, self.public_key_path)
|
||||
])
|
||||
except (IOError, OSError):
|
||||
self.module.fail_json(
|
||||
msg="The public key is missing or does not match the private key. " +
|
||||
"Unable to regenerate the public key."
|
||||
)
|
||||
|
||||
if self.comment:
|
||||
self._update_comment()
|
||||
|
||||
def _create_temp_public_key(self, content):
|
||||
temp_public_key = os.path.join(self.module.tmpdir, os.path.basename(self.public_key_path))
|
||||
|
||||
default_permissions = 0o644
|
||||
existing_permissions = file_mode(self.public_key_path)
|
||||
|
||||
try:
|
||||
secure_write(temp_public_key, existing_permissions or default_permissions, to_bytes(content))
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
self.module.add_cleanup_file(temp_public_key)
|
||||
|
||||
return temp_public_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _update_comment(self):
|
||||
pass
|
||||
|
||||
def _should_remove(self):
|
||||
return self._private_key_exists() or self._public_key_exists()
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _remove(self):
|
||||
try:
|
||||
if self._private_key_exists():
|
||||
os.remove(self.private_key_path)
|
||||
if self._public_key_exists():
|
||||
os.remove(self.public_key_path)
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
@property
|
||||
def _result(self):
|
||||
private_key = self.private_key or self.original_private_key
|
||||
public_key = self.public_key or self.original_public_key
|
||||
|
||||
return {
|
||||
'size': self.size,
|
||||
'type': self.type,
|
||||
'filename': self.private_key_path,
|
||||
'fingerprint': private_key.fingerprint if private_key else '',
|
||||
'public_key': str(public_key) if public_key else '',
|
||||
'comment': public_key.comment if public_key else '',
|
||||
}
|
||||
|
||||
@property
|
||||
def diff(self):
|
||||
before = self.original_private_key.to_dict() if self.original_private_key else {}
|
||||
before.update(self.original_public_key.to_dict() if self.original_public_key else {})
|
||||
|
||||
after = self.private_key.to_dict() if self.private_key else {}
|
||||
after.update(self.public_key.to_dict() if self.public_key else {})
|
||||
|
||||
return {
|
||||
'before': before,
|
||||
'after': after,
|
||||
}
|
||||
|
||||
|
||||
class KeypairBackendOpensshBin(KeypairBackend):
|
||||
def __init__(self, module):
|
||||
super(KeypairBackendOpensshBin, self).__init__(module)
|
||||
|
||||
self.ssh_keygen = KeygenCommand(self.module)
|
||||
|
||||
def _generate_keypair(self, private_key_path):
|
||||
self.ssh_keygen.generate_keypair(private_key_path, self.size, self.type, self.comment)
|
||||
|
||||
def _get_private_key(self):
|
||||
private_key_content = self.ssh_keygen.get_private_key(self.private_key_path)[1]
|
||||
return PrivateKey.from_string(private_key_content)
|
||||
|
||||
def _get_public_key(self):
|
||||
public_key_content = self.ssh_keygen.get_matching_public_key(self.private_key_path)[1]
|
||||
return PublicKey.from_string(public_key_content)
|
||||
|
||||
def _private_key_readable(self):
|
||||
rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(self.private_key_path)
|
||||
return not (rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed'))
|
||||
|
||||
def _update_comment(self):
|
||||
try:
|
||||
self.ssh_keygen.update_comment(self.private_key_path, self.comment)
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
|
||||
class KeypairBackendCryptography(KeypairBackend):
|
||||
def __init__(self, module):
|
||||
super(KeypairBackendCryptography, self).__init__(module)
|
||||
|
||||
if self.type == 'rsa1':
|
||||
self.module.fail_json(msg="RSA1 keys are not supported by the cryptography backend")
|
||||
|
||||
self.passphrase = to_bytes(module.params['passphrase']) if module.params['passphrase'] else None
|
||||
self.private_key_format = self._get_key_format(module.params['private_key_format'])
|
||||
|
||||
def _get_key_format(self, key_format):
|
||||
result = 'SSH'
|
||||
|
||||
if key_format == 'auto':
|
||||
# Default to OpenSSH 7.8 compatibility when OpenSSH is not installed
|
||||
ssh_version = self._get_ssh_version() or "7.8"
|
||||
|
||||
if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519':
|
||||
# OpenSSH made SSH formatted private keys available in version 6.5,
|
||||
# but still defaulted to PKCS1 format with the exception of ed25519 keys
|
||||
result = 'PKCS1'
|
||||
|
||||
if result == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT:
|
||||
self.module.fail_json(
|
||||
msg=missing_required_lib(
|
||||
'cryptography >= 3.0',
|
||||
reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " +
|
||||
"or for ed25519 keys"
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _generate_keypair(self, private_key_path):
|
||||
keypair = OpensshKeypair.generate(
|
||||
keytype=self.type,
|
||||
size=self.size,
|
||||
passphrase=self.passphrase,
|
||||
comment=self.comment or '',
|
||||
)
|
||||
|
||||
encoded_private_key = OpensshKeypair.encode_openssh_privatekey(
|
||||
keypair.asymmetric_keypair, self.private_key_format
|
||||
)
|
||||
secure_write(private_key_path, 0o600, encoded_private_key)
|
||||
|
||||
public_key_path = private_key_path + '.pub'
|
||||
secure_write(public_key_path, 0o644, keypair.public_key)
|
||||
|
||||
def _get_private_key(self):
|
||||
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
|
||||
return PrivateKey(
|
||||
size=keypair.size,
|
||||
key_type=keypair.key_type,
|
||||
fingerprint=keypair.fingerprint,
|
||||
)
|
||||
|
||||
def _get_public_key(self):
|
||||
try:
|
||||
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
except OpenSSHError:
|
||||
# Simulates the null output of ssh-keygen
|
||||
return ""
|
||||
|
||||
return PublicKey.from_string(to_text(keypair.public_key))
|
||||
|
||||
def _private_key_readable(self):
|
||||
try:
|
||||
OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
except (InvalidPrivateKeyFileError, InvalidPassphraseError):
|
||||
return False
|
||||
|
||||
# Cryptography >= 3.0 uses a SSH key loader which does not raise an exception when a passphrase is provided
|
||||
# when loading an unencrypted key
|
||||
if self.passphrase:
|
||||
try:
|
||||
OpensshKeypair.load(path=self.private_key_path, passphrase=None, no_public_key=True)
|
||||
except (InvalidPrivateKeyFileError, InvalidPassphraseError):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _update_comment(self):
|
||||
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
try:
|
||||
keypair.comment = self.comment
|
||||
except InvalidCommentError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
try:
|
||||
temp_public_key = self._create_temp_public_key(keypair.public_key + b'\n')
|
||||
self._safe_secure_move([(temp_public_key, self.public_key_path)])
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
|
||||
def select_backend(module, backend):
|
||||
can_use_cryptography = HAS_OPENSSH_SUPPORT
|
||||
can_use_opensshbin = bool(module.get_bin_path('ssh-keygen'))
|
||||
|
||||
if backend == 'auto':
|
||||
if can_use_opensshbin and not module.params['passphrase']:
|
||||
backend = 'opensshbin'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
else:
|
||||
module.fail_json(msg="Cannot find either the OpenSSH binary in the PATH " +
|
||||
"or cryptography >= 2.6 installed on this system")
|
||||
|
||||
if backend == 'opensshbin':
|
||||
if not can_use_opensshbin:
|
||||
module.fail_json(msg="Cannot find the OpenSSH binary in the PATH")
|
||||
return backend, KeypairBackendOpensshBin(module)
|
||||
elif backend == 'cryptography':
|
||||
if not can_use_cryptography:
|
||||
module.fail_json(msg=missing_required_lib("cryptography >= 2.6"))
|
||||
return backend, KeypairBackendCryptography(module)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
677
plugins/module_utils/openssh/certificate.py
Normal file
677
plugins/module_utils/openssh/certificate.py
Normal file
@@ -0,0 +1,677 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
# Protocol References
|
||||
# -------------------
|
||||
# https://datatracker.ietf.org/doc/html/rfc4251
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253
|
||||
# https://datatracker.ietf.org/doc/html/rfc5656
|
||||
# https://datatracker.ietf.org/doc/html/rfc8032
|
||||
# https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
|
||||
#
|
||||
# Inspired by:
|
||||
# ------------
|
||||
# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
|
||||
# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
OpensshParser,
|
||||
_OpensshWriter,
|
||||
)
|
||||
|
||||
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
|
||||
_USER_TYPE = 1
|
||||
_HOST_TYPE = 2
|
||||
|
||||
_SSH_TYPE_STRINGS = {
|
||||
'rsa': b"ssh-rsa",
|
||||
'dsa': b"ssh-dss",
|
||||
'ecdsa-nistp256': b"ecdsa-sha2-nistp256",
|
||||
'ecdsa-nistp384': b"ecdsa-sha2-nistp384",
|
||||
'ecdsa-nistp521': b"ecdsa-sha2-nistp521",
|
||||
'ed25519': b"ssh-ed25519",
|
||||
}
|
||||
_CERT_SUFFIX_V01 = b"-cert-v01@openssh.com"
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc5656#section-6.1
|
||||
_ECDSA_CURVE_IDENTIFIERS = {
|
||||
'ecdsa-nistp256': b'nistp256',
|
||||
'ecdsa-nistp384': b'nistp384',
|
||||
'ecdsa-nistp521': b'nistp521',
|
||||
}
|
||||
_ECDSA_CURVE_IDENTIFIERS_LOOKUP = {
|
||||
b'nistp256': 'ecdsa-nistp256',
|
||||
b'nistp384': 'ecdsa-nistp384',
|
||||
b'nistp521': 'ecdsa-nistp521',
|
||||
}
|
||||
|
||||
_ALWAYS = datetime(1970, 1, 1)
|
||||
_FOREVER = datetime.max
|
||||
|
||||
_CRITICAL_OPTIONS = (
|
||||
'force-command',
|
||||
'source-address',
|
||||
'verify-required',
|
||||
)
|
||||
|
||||
_DIRECTIVES = (
|
||||
'clear',
|
||||
'no-x11-forwarding',
|
||||
'no-agent-forwarding',
|
||||
'no-port-forwarding',
|
||||
'no-pty',
|
||||
'no-user-rc',
|
||||
)
|
||||
|
||||
_EXTENSIONS = (
|
||||
'permit-x11-forwarding',
|
||||
'permit-agent-forwarding',
|
||||
'permit-port-forwarding',
|
||||
'permit-pty',
|
||||
'permit-user-rc'
|
||||
)
|
||||
|
||||
if six.PY3:
|
||||
long = int
|
||||
|
||||
|
||||
class OpensshCertificateTimeParameters(object):
|
||||
def __init__(self, valid_from, valid_to):
|
||||
self._valid_from = self.to_datetime(valid_from)
|
||||
self._valid_to = self.to_datetime(valid_to)
|
||||
|
||||
if self._valid_from > self._valid_to:
|
||||
raise ValueError("Valid from: %s must not be greater than Valid to: %s" % (valid_from, valid_to))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
else:
|
||||
return self._valid_from == other._valid_from and self._valid_to == other._valid_to
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@property
|
||||
def validity_string(self):
|
||||
if not (self._valid_from == _ALWAYS and self._valid_to == _FOREVER):
|
||||
return "%s:%s" % (
|
||||
self.valid_from(date_format='openssh'), self.valid_to(date_format='openssh')
|
||||
)
|
||||
return ""
|
||||
|
||||
def valid_from(self, date_format):
|
||||
return self.format_datetime(self._valid_from, date_format)
|
||||
|
||||
def valid_to(self, date_format):
|
||||
return self.format_datetime(self._valid_to, date_format)
|
||||
|
||||
def within_range(self, valid_at):
|
||||
if valid_at is not None:
|
||||
valid_at_datetime = self.to_datetime(valid_at)
|
||||
return self._valid_from <= valid_at_datetime <= self._valid_to
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(dt, date_format):
|
||||
if date_format in ('human_readable', 'openssh'):
|
||||
if dt == _ALWAYS:
|
||||
result = 'always'
|
||||
elif dt == _FOREVER:
|
||||
result = 'forever'
|
||||
else:
|
||||
result = dt.isoformat() if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S")
|
||||
elif date_format == 'timestamp':
|
||||
td = dt - _ALWAYS
|
||||
result = int((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6)
|
||||
else:
|
||||
raise ValueError("%s is not a valid format" % date_format)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def to_datetime(time_string_or_timestamp):
|
||||
try:
|
||||
if isinstance(time_string_or_timestamp, six.string_types):
|
||||
result = OpensshCertificateTimeParameters._time_string_to_datetime(time_string_or_timestamp.strip())
|
||||
elif isinstance(time_string_or_timestamp, (long, int)):
|
||||
result = OpensshCertificateTimeParameters._timestamp_to_datetime(time_string_or_timestamp)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Value must be of type (str, unicode, int, long) not %s" % type(time_string_or_timestamp)
|
||||
)
|
||||
except ValueError:
|
||||
raise
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _timestamp_to_datetime(timestamp):
|
||||
if timestamp == 0x0:
|
||||
result = _ALWAYS
|
||||
elif timestamp == 0xFFFFFFFFFFFFFFFF:
|
||||
result = _FOREVER
|
||||
else:
|
||||
try:
|
||||
result = datetime.utcfromtimestamp(timestamp)
|
||||
except OverflowError as e:
|
||||
raise ValueError
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _time_string_to_datetime(time_string):
|
||||
result = None
|
||||
if time_string == 'always':
|
||||
result = _ALWAYS
|
||||
elif time_string == 'forever':
|
||||
result = _FOREVER
|
||||
elif is_relative_time_string(time_string):
|
||||
result = convert_relative_to_datetime(time_string)
|
||||
else:
|
||||
for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
||||
try:
|
||||
result = datetime.strptime(time_string, time_format)
|
||||
except ValueError:
|
||||
pass
|
||||
if result is None:
|
||||
raise ValueError
|
||||
return result
|
||||
|
||||
|
||||
class OpensshCertificateOption(object):
|
||||
def __init__(self, option_type, name, data):
|
||||
if option_type not in ('critical', 'extension'):
|
||||
raise ValueError("type must be either 'critical' or 'extension'")
|
||||
|
||||
if not isinstance(name, six.string_types):
|
||||
raise TypeError("name must be a string not %s" % type(name))
|
||||
|
||||
if not isinstance(data, six.string_types):
|
||||
raise TypeError("data must be a string not %s" % type(data))
|
||||
|
||||
self._option_type = option_type
|
||||
self._name = name.lower()
|
||||
self._data = data
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
|
||||
return all([
|
||||
self._option_type == other._option_type,
|
||||
self._name == other._name,
|
||||
self._data == other._data,
|
||||
])
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self._option_type, self._name, self._data))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __str__(self):
|
||||
if self._data:
|
||||
return "%s=%s" % (self._name, self._data)
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._option_type
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, option_string):
|
||||
if not isinstance(option_string, six.string_types):
|
||||
raise ValueError("option_string must be a string not %s" % type(option_string))
|
||||
option_type = None
|
||||
|
||||
if ':' in option_string:
|
||||
option_type, value = option_string.strip().split(':', 1)
|
||||
if '=' in value:
|
||||
name, data = value.split('=', 1)
|
||||
else:
|
||||
name, data = value, ''
|
||||
elif '=' in option_string:
|
||||
name, data = option_string.strip().split('=', 1)
|
||||
else:
|
||||
name, data = option_string.strip(), ''
|
||||
|
||||
return cls(
|
||||
option_type=option_type or get_option_type(name.lower()),
|
||||
name=name,
|
||||
data=data
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OpensshCertificateInfo:
|
||||
"""Encapsulates all certificate information which is signed by a CA key"""
|
||||
def __init__(self,
|
||||
nonce=None,
|
||||
serial=None,
|
||||
cert_type=None,
|
||||
key_id=None,
|
||||
principals=None,
|
||||
valid_after=None,
|
||||
valid_before=None,
|
||||
critical_options=None,
|
||||
extensions=None,
|
||||
reserved=None,
|
||||
signing_key=None):
|
||||
self.nonce = nonce
|
||||
self.serial = serial
|
||||
self._cert_type = cert_type
|
||||
self.key_id = key_id
|
||||
self.principals = principals
|
||||
self.valid_after = valid_after
|
||||
self.valid_before = valid_before
|
||||
self.critical_options = critical_options
|
||||
self.extensions = extensions
|
||||
self.reserved = reserved
|
||||
self.signing_key = signing_key
|
||||
|
||||
self.type_string = None
|
||||
|
||||
@property
|
||||
def cert_type(self):
|
||||
if self._cert_type == _USER_TYPE:
|
||||
return 'user'
|
||||
elif self._cert_type == _HOST_TYPE:
|
||||
return 'host'
|
||||
else:
|
||||
return ''
|
||||
|
||||
@cert_type.setter
|
||||
def cert_type(self, cert_type):
|
||||
if cert_type == 'user' or cert_type == _USER_TYPE:
|
||||
self._cert_type = _USER_TYPE
|
||||
elif cert_type == 'host' or cert_type == _HOST_TYPE:
|
||||
self._cert_type = _HOST_TYPE
|
||||
else:
|
||||
raise ValueError("%s is not a valid certificate type" % cert_type)
|
||||
|
||||
def signing_key_fingerprint(self):
|
||||
return fingerprint(self.signing_key)
|
||||
|
||||
@abc.abstractmethod
|
||||
def public_key_fingerprint(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def parse_public_numbers(self, parser):
|
||||
pass
|
||||
|
||||
|
||||
class OpensshRSACertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, e=None, n=None, **kwargs):
|
||||
super(OpensshRSACertificateInfo, self).__init__(**kwargs)
|
||||
self.type_string = _SSH_TYPE_STRINGS['rsa'] + _CERT_SUFFIX_V01
|
||||
self.e = e
|
||||
self.n = n
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
def public_key_fingerprint(self):
|
||||
if any([self.e is None, self.n is None]):
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS['rsa'])
|
||||
writer.mpint(self.e)
|
||||
writer.mpint(self.n)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.e = parser.mpint()
|
||||
self.n = parser.mpint()
|
||||
|
||||
|
||||
class OpensshDSACertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, p=None, q=None, g=None, y=None, **kwargs):
|
||||
super(OpensshDSACertificateInfo, self).__init__(**kwargs)
|
||||
self.type_string = _SSH_TYPE_STRINGS['dsa'] + _CERT_SUFFIX_V01
|
||||
self.p = p
|
||||
self.q = q
|
||||
self.g = g
|
||||
self.y = y
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
def public_key_fingerprint(self):
|
||||
if any([self.p is None, self.q is None, self.g is None, self.y is None]):
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS['dsa'])
|
||||
writer.mpint(self.p)
|
||||
writer.mpint(self.q)
|
||||
writer.mpint(self.g)
|
||||
writer.mpint(self.y)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.p = parser.mpint()
|
||||
self.q = parser.mpint()
|
||||
self.g = parser.mpint()
|
||||
self.y = parser.mpint()
|
||||
|
||||
|
||||
class OpensshECDSACertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, curve=None, public_key=None, **kwargs):
|
||||
super(OpensshECDSACertificateInfo, self).__init__(**kwargs)
|
||||
self._curve = None
|
||||
if curve is not None:
|
||||
self.curve = curve
|
||||
|
||||
self.public_key = public_key
|
||||
|
||||
@property
|
||||
def curve(self):
|
||||
return self._curve
|
||||
|
||||
@curve.setter
|
||||
def curve(self, curve):
|
||||
if curve in _ECDSA_CURVE_IDENTIFIERS.values():
|
||||
self._curve = curve
|
||||
self.type_string = _SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[curve]] + _CERT_SUFFIX_V01
|
||||
else:
|
||||
raise ValueError(
|
||||
"Curve must be one of %s" % (b','.join(list(_ECDSA_CURVE_IDENTIFIERS.values()))).decode('UTF-8')
|
||||
)
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
def public_key_fingerprint(self):
|
||||
if any([self.curve is None, self.public_key is None]):
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[self.curve]])
|
||||
writer.string(self.curve)
|
||||
writer.string(self.public_key)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.curve = parser.string()
|
||||
self.public_key = parser.string()
|
||||
|
||||
|
||||
class OpensshED25519CertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, pk=None, **kwargs):
|
||||
super(OpensshED25519CertificateInfo, self).__init__(**kwargs)
|
||||
self.type_string = _SSH_TYPE_STRINGS['ed25519'] + _CERT_SUFFIX_V01
|
||||
self.pk = pk
|
||||
|
||||
def public_key_fingerprint(self):
|
||||
if self.pk is None:
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS['ed25519'])
|
||||
writer.string(self.pk)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.pk = parser.string()
|
||||
|
||||
|
||||
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
|
||||
class OpensshCertificate(object):
|
||||
"""Encapsulates a formatted OpenSSH certificate including signature and signing key"""
|
||||
def __init__(self, cert_info, signature):
|
||||
|
||||
self._cert_info = cert_info
|
||||
self.signature = signature
|
||||
|
||||
@classmethod
|
||||
def load(cls, path):
|
||||
if not os.path.exists(path):
|
||||
raise ValueError("%s is not a valid path." % path)
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as cert_file:
|
||||
data = cert_file.read()
|
||||
except (IOError, OSError) as e:
|
||||
raise ValueError("%s cannot be opened for reading: %s" % (path, e))
|
||||
|
||||
try:
|
||||
format_identifier, b64_cert = data.split(b' ')[:2]
|
||||
cert = binascii.a2b_base64(b64_cert)
|
||||
except (binascii.Error, ValueError):
|
||||
raise ValueError("Certificate not in OpenSSH format")
|
||||
|
||||
for key_type, string in _SSH_TYPE_STRINGS.items():
|
||||
if format_identifier == string + _CERT_SUFFIX_V01:
|
||||
pub_key_type = key_type
|
||||
break
|
||||
else:
|
||||
raise ValueError("Invalid certificate format identifier: %s" % format_identifier)
|
||||
|
||||
parser = OpensshParser(cert)
|
||||
|
||||
if format_identifier != parser.string():
|
||||
raise ValueError("Certificate formats do not match")
|
||||
|
||||
try:
|
||||
cert_info = cls._parse_cert_info(pub_key_type, parser)
|
||||
signature = parser.string()
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError("Invalid certificate data: %s" % e)
|
||||
|
||||
if parser.remaining_bytes():
|
||||
raise ValueError(
|
||||
"%s bytes of additional data was not parsed while loading %s" % (parser.remaining_bytes(), path)
|
||||
)
|
||||
|
||||
return cls(
|
||||
cert_info=cert_info,
|
||||
signature=signature,
|
||||
)
|
||||
|
||||
@property
|
||||
def type_string(self):
|
||||
return to_text(self._cert_info.type_string)
|
||||
|
||||
@property
|
||||
def nonce(self):
|
||||
return self._cert_info.nonce
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
return to_text(self._cert_info.public_key_fingerprint())
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
return self._cert_info.serial
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._cert_info.cert_type
|
||||
|
||||
@property
|
||||
def key_id(self):
|
||||
return to_text(self._cert_info.key_id)
|
||||
|
||||
@property
|
||||
def principals(self):
|
||||
return [to_text(p) for p in self._cert_info.principals]
|
||||
|
||||
@property
|
||||
def valid_after(self):
|
||||
return self._cert_info.valid_after
|
||||
|
||||
@property
|
||||
def valid_before(self):
|
||||
return self._cert_info.valid_before
|
||||
|
||||
@property
|
||||
def critical_options(self):
|
||||
return [
|
||||
OpensshCertificateOption('critical', to_text(n), to_text(d)) for n, d in self._cert_info.critical_options
|
||||
]
|
||||
|
||||
@property
|
||||
def extensions(self):
|
||||
return [OpensshCertificateOption('extension', to_text(n), to_text(d)) for n, d in self._cert_info.extensions]
|
||||
|
||||
@property
|
||||
def reserved(self):
|
||||
return self._cert_info.reserved
|
||||
|
||||
@property
|
||||
def signing_key(self):
|
||||
return to_text(self._cert_info.signing_key_fingerprint())
|
||||
|
||||
@property
|
||||
def signature_type(self):
|
||||
signature_data = OpensshParser.signature_data(self.signature)
|
||||
return to_text(signature_data['signature_type'])
|
||||
|
||||
@staticmethod
|
||||
def _parse_cert_info(pub_key_type, parser):
|
||||
cert_info = get_cert_info_object(pub_key_type)
|
||||
cert_info.nonce = parser.string()
|
||||
cert_info.parse_public_numbers(parser)
|
||||
cert_info.serial = parser.uint64()
|
||||
cert_info.cert_type = parser.uint32()
|
||||
cert_info.key_id = parser.string()
|
||||
cert_info.principals = parser.string_list()
|
||||
cert_info.valid_after = parser.uint64()
|
||||
cert_info.valid_before = parser.uint64()
|
||||
cert_info.critical_options = parser.option_list()
|
||||
cert_info.extensions = parser.option_list()
|
||||
cert_info.reserved = parser.string()
|
||||
cert_info.signing_key = parser.string()
|
||||
|
||||
return cert_info
|
||||
|
||||
def to_dict(self):
|
||||
time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=self.valid_after,
|
||||
valid_to=self.valid_before
|
||||
)
|
||||
return {
|
||||
'type_string': self.type_string,
|
||||
'nonce': self.nonce,
|
||||
'serial': self.serial,
|
||||
'cert_type': self.type,
|
||||
'identifier': self.key_id,
|
||||
'principals': self.principals,
|
||||
'valid_after': time_parameters.valid_from(date_format='human_readable'),
|
||||
'valid_before': time_parameters.valid_to(date_format='human_readable'),
|
||||
'critical_options': [str(critical_option) for critical_option in self.critical_options],
|
||||
'extensions': [str(extension) for extension in self.extensions],
|
||||
'reserved': self.reserved,
|
||||
'public_key': self.public_key,
|
||||
'signing_key': self.signing_key,
|
||||
}
|
||||
|
||||
|
||||
def apply_directives(directives):
|
||||
if any(d not in _DIRECTIVES for d in directives):
|
||||
raise ValueError("directives must be one of %s" % ", ".join(_DIRECTIVES))
|
||||
|
||||
directive_to_option = {
|
||||
'no-x11-forwarding': OpensshCertificateOption('extension', 'permit-x11-forwarding', ''),
|
||||
'no-agent-forwarding': OpensshCertificateOption('extension', 'permit-agent-forwarding', ''),
|
||||
'no-port-forwarding': OpensshCertificateOption('extension', 'permit-port-forwarding', ''),
|
||||
'no-pty': OpensshCertificateOption('extension', 'permit-pty', ''),
|
||||
'no-user-rc': OpensshCertificateOption('extension', 'permit-user-rc', ''),
|
||||
}
|
||||
|
||||
if 'clear' in directives:
|
||||
return []
|
||||
else:
|
||||
return list(set(default_options()) - set(directive_to_option[d] for d in directives))
|
||||
|
||||
|
||||
def default_options():
|
||||
return [OpensshCertificateOption('extension', name, '') for name in _EXTENSIONS]
|
||||
|
||||
|
||||
def fingerprint(public_key):
|
||||
"""Generates a SHA256 hash and formats output to resemble ``ssh-keygen``"""
|
||||
h = sha256()
|
||||
h.update(public_key)
|
||||
return b'SHA256:' + b64encode(h.digest()).rstrip(b'=')
|
||||
|
||||
|
||||
def get_cert_info_object(key_type):
|
||||
if key_type == 'rsa':
|
||||
cert_info = OpensshRSACertificateInfo()
|
||||
elif key_type == 'dsa':
|
||||
cert_info = OpensshDSACertificateInfo()
|
||||
elif key_type in ('ecdsa-nistp256', 'ecdsa-nistp384', 'ecdsa-nistp521'):
|
||||
cert_info = OpensshECDSACertificateInfo()
|
||||
elif key_type == 'ed25519':
|
||||
cert_info = OpensshED25519CertificateInfo()
|
||||
else:
|
||||
raise ValueError("%s is not a valid key type" % key_type)
|
||||
|
||||
return cert_info
|
||||
|
||||
|
||||
def get_option_type(name):
|
||||
if name in _CRITICAL_OPTIONS:
|
||||
result = 'critical'
|
||||
elif name in _EXTENSIONS:
|
||||
result = 'extension'
|
||||
else:
|
||||
raise ValueError("%s is not a valid option. " % name +
|
||||
"Custom options must start with 'critical:' or 'extension:' to indicate type")
|
||||
return result
|
||||
|
||||
|
||||
def is_relative_time_string(time_string):
|
||||
return time_string.startswith("+") or time_string.startswith("-")
|
||||
|
||||
|
||||
def parse_option_list(option_list):
|
||||
critical_options = []
|
||||
directives = []
|
||||
extensions = []
|
||||
|
||||
for option in option_list:
|
||||
if option.lower() in _DIRECTIVES:
|
||||
directives.append(option.lower())
|
||||
else:
|
||||
option_object = OpensshCertificateOption.from_string(option)
|
||||
if option_object.type == 'critical':
|
||||
critical_options.append(option_object)
|
||||
else:
|
||||
extensions.append(option_object)
|
||||
|
||||
return critical_options, list(set(extensions + apply_directives(directives)))
|
||||
696
plugins/module_utils/openssh/cryptography.py
Normal file
696
plugins/module_utils/openssh/cryptography.py
Normal file
@@ -0,0 +1,696 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
from base64 import b64encode, b64decode
|
||||
from getpass import getuser
|
||||
from socket import gethostname
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from cryptography import __version__ as CRYPTOGRAPHY_VERSION
|
||||
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
|
||||
from cryptography.hazmat.backends.openssl import backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
|
||||
if LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion("3.0"):
|
||||
HAS_OPENSSH_PRIVATE_FORMAT = True
|
||||
else:
|
||||
HAS_OPENSSH_PRIVATE_FORMAT = False
|
||||
|
||||
HAS_OPENSSH_SUPPORT = True
|
||||
|
||||
_ALGORITHM_PARAMETERS = {
|
||||
'rsa': {
|
||||
'default_size': 2048,
|
||||
'valid_sizes': range(1024, 16384),
|
||||
'signer_params': {
|
||||
'padding': padding.PSS(
|
||||
mgf=padding.MGF1(hashes.SHA256()),
|
||||
salt_length=padding.PSS.MAX_LENGTH,
|
||||
),
|
||||
'algorithm': hashes.SHA256(),
|
||||
},
|
||||
},
|
||||
'dsa': {
|
||||
'default_size': 1024,
|
||||
'valid_sizes': [1024],
|
||||
'signer_params': {
|
||||
'algorithm': hashes.SHA256(),
|
||||
},
|
||||
},
|
||||
'ed25519': {
|
||||
'default_size': 256,
|
||||
'valid_sizes': [256],
|
||||
'signer_params': {},
|
||||
},
|
||||
'ecdsa': {
|
||||
'default_size': 256,
|
||||
'valid_sizes': [256, 384, 521],
|
||||
'signer_params': {
|
||||
'signature_algorithm': ec.ECDSA(hashes.SHA256()),
|
||||
},
|
||||
'curves': {
|
||||
256: ec.SECP256R1(),
|
||||
384: ec.SECP384R1(),
|
||||
521: ec.SECP521R1(),
|
||||
}
|
||||
}
|
||||
}
|
||||
except ImportError:
|
||||
HAS_OPENSSH_PRIVATE_FORMAT = False
|
||||
HAS_OPENSSH_SUPPORT = False
|
||||
CRYPTOGRAPHY_VERSION = "0.0"
|
||||
_ALGORITHM_PARAMETERS = {}
|
||||
|
||||
_TEXT_ENCODING = 'UTF-8'
|
||||
|
||||
|
||||
class OpenSSHError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAlgorithmError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidCommentError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDataError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPrivateKeyFileError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPublicKeyFileError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidKeyFormatError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidKeySizeError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidKeyTypeError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPassphraseError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSignatureError(OpenSSHError):
|
||||
pass
|
||||
|
||||
|
||||
class AsymmetricKeypair(object):
|
||||
"""Container for newly generated asymmetric key pairs or those loaded from existing files"""
|
||||
|
||||
@classmethod
|
||||
def generate(cls, keytype='rsa', size=None, passphrase=None):
|
||||
"""Returns an Asymmetric_Keypair object generated with the supplied parameters
|
||||
or defaults to an unencrypted RSA-2048 key
|
||||
|
||||
:keytype: One of rsa, dsa, ecdsa, ed25519
|
||||
:size: The key length for newly generated keys
|
||||
:passphrase: Secret of type Bytes used to encrypt the private key being generated
|
||||
"""
|
||||
|
||||
if keytype not in _ALGORITHM_PARAMETERS.keys():
|
||||
raise InvalidKeyTypeError(
|
||||
"%s is not a valid keytype. Valid keytypes are %s" % (
|
||||
keytype, ", ".join(_ALGORITHM_PARAMETERS.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if not size:
|
||||
size = _ALGORITHM_PARAMETERS[keytype]['default_size']
|
||||
else:
|
||||
if size not in _ALGORITHM_PARAMETERS[keytype]['valid_sizes']:
|
||||
raise InvalidKeySizeError(
|
||||
"%s is not a valid key size for %s keys" % (size, keytype)
|
||||
)
|
||||
|
||||
if passphrase:
|
||||
encryption_algorithm = get_encryption_algorithm(passphrase)
|
||||
else:
|
||||
encryption_algorithm = serialization.NoEncryption()
|
||||
|
||||
if keytype == 'rsa':
|
||||
privatekey = rsa.generate_private_key(
|
||||
# Public exponent should always be 65537 to prevent issues
|
||||
# if improper padding is used during signing
|
||||
public_exponent=65537,
|
||||
key_size=size,
|
||||
backend=backend,
|
||||
)
|
||||
elif keytype == 'dsa':
|
||||
privatekey = dsa.generate_private_key(
|
||||
key_size=size,
|
||||
backend=backend,
|
||||
)
|
||||
elif keytype == 'ed25519':
|
||||
privatekey = Ed25519PrivateKey.generate()
|
||||
elif keytype == 'ecdsa':
|
||||
privatekey = ec.generate_private_key(
|
||||
_ALGORITHM_PARAMETERS['ecdsa']['curves'][size],
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
publickey = privatekey.public_key()
|
||||
|
||||
return cls(
|
||||
keytype=keytype,
|
||||
size=size,
|
||||
privatekey=privatekey,
|
||||
publickey=publickey,
|
||||
encryption_algorithm=encryption_algorithm
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, passphrase=None, private_key_format='PEM', public_key_format='PEM', no_public_key=False):
|
||||
"""Returns an Asymmetric_Keypair object loaded from the supplied file path
|
||||
|
||||
:path: A path to an existing private key to be loaded
|
||||
:passphrase: Secret of type bytes used to decrypt the private key being loaded
|
||||
:private_key_format: Format of private key to be loaded
|
||||
:public_key_format: Format of public key to be loaded
|
||||
:no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
|
||||
"""
|
||||
|
||||
if passphrase:
|
||||
encryption_algorithm = get_encryption_algorithm(passphrase)
|
||||
else:
|
||||
encryption_algorithm = serialization.NoEncryption()
|
||||
|
||||
privatekey = load_privatekey(path, passphrase, private_key_format)
|
||||
if no_public_key:
|
||||
publickey = privatekey.public_key()
|
||||
else:
|
||||
publickey = load_publickey(path + '.pub', public_key_format)
|
||||
|
||||
# Ed25519 keys are always of size 256 and do not have a key_size attribute
|
||||
if isinstance(privatekey, Ed25519PrivateKey):
|
||||
size = _ALGORITHM_PARAMETERS['ed25519']['default_size']
|
||||
else:
|
||||
size = privatekey.key_size
|
||||
|
||||
if isinstance(privatekey, rsa.RSAPrivateKey):
|
||||
keytype = 'rsa'
|
||||
elif isinstance(privatekey, dsa.DSAPrivateKey):
|
||||
keytype = 'dsa'
|
||||
elif isinstance(privatekey, ec.EllipticCurvePrivateKey):
|
||||
keytype = 'ecdsa'
|
||||
elif isinstance(privatekey, Ed25519PrivateKey):
|
||||
keytype = 'ed25519'
|
||||
else:
|
||||
raise InvalidKeyTypeError("Key type '%s' is not supported" % type(privatekey))
|
||||
|
||||
return cls(
|
||||
keytype=keytype,
|
||||
size=size,
|
||||
privatekey=privatekey,
|
||||
publickey=publickey,
|
||||
encryption_algorithm=encryption_algorithm
|
||||
)
|
||||
|
||||
def __init__(self, keytype, size, privatekey, publickey, encryption_algorithm):
|
||||
"""
|
||||
:keytype: One of rsa, dsa, ecdsa, ed25519
|
||||
:size: The key length for the private key of this key pair
|
||||
:privatekey: Private key object of this key pair
|
||||
:publickey: Public key object of this key pair
|
||||
:encryption_algorithm: Hashed secret used to encrypt the private key of this key pair
|
||||
"""
|
||||
|
||||
self.__size = size
|
||||
self.__keytype = keytype
|
||||
self.__privatekey = privatekey
|
||||
self.__publickey = publickey
|
||||
self.__encryption_algorithm = encryption_algorithm
|
||||
|
||||
try:
|
||||
self.verify(self.sign(b'message'), b'message')
|
||||
except InvalidSignatureError:
|
||||
raise InvalidPublicKeyFileError(
|
||||
"The private key and public key of this keypair do not match"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, AsymmetricKeypair):
|
||||
return NotImplemented
|
||||
|
||||
return (compare_publickeys(self.public_key, other.public_key) and
|
||||
compare_encryption_algorithms(self.encryption_algorithm, other.encryption_algorithm))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
"""Returns the private key of this key pair"""
|
||||
|
||||
return self.__privatekey
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
"""Returns the public key of this key pair"""
|
||||
|
||||
return self.__publickey
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""Returns the size of the private key of this key pair"""
|
||||
|
||||
return self.__size
|
||||
|
||||
@property
|
||||
def key_type(self):
|
||||
"""Returns the key type of this key pair"""
|
||||
|
||||
return self.__keytype
|
||||
|
||||
@property
|
||||
def encryption_algorithm(self):
|
||||
"""Returns the key encryption algorithm of this key pair"""
|
||||
|
||||
return self.__encryption_algorithm
|
||||
|
||||
def sign(self, data):
|
||||
"""Returns signature of data signed with the private key of this key pair
|
||||
|
||||
:data: byteslike data to sign
|
||||
"""
|
||||
|
||||
try:
|
||||
signature = self.__privatekey.sign(
|
||||
data,
|
||||
**_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
|
||||
)
|
||||
except TypeError as e:
|
||||
raise InvalidDataError(e)
|
||||
|
||||
return signature
|
||||
|
||||
def verify(self, signature, data):
|
||||
"""Verifies that the signature associated with the provided data was signed
|
||||
by the private key of this key pair.
|
||||
|
||||
:signature: signature to verify
|
||||
:data: byteslike data signed by the provided signature
|
||||
"""
|
||||
try:
|
||||
return self.__publickey.verify(
|
||||
signature,
|
||||
data,
|
||||
**_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
|
||||
)
|
||||
except InvalidSignature:
|
||||
raise InvalidSignatureError
|
||||
|
||||
def update_passphrase(self, passphrase=None):
|
||||
"""Updates the encryption algorithm of this key pair
|
||||
|
||||
:passphrase: Byte secret used to encrypt this key pair
|
||||
"""
|
||||
|
||||
if passphrase:
|
||||
self.__encryption_algorithm = get_encryption_algorithm(passphrase)
|
||||
else:
|
||||
self.__encryption_algorithm = serialization.NoEncryption()
|
||||
|
||||
|
||||
class OpensshKeypair(object):
|
||||
"""Container for OpenSSH encoded asymmetric key pairs"""
|
||||
|
||||
@classmethod
|
||||
def generate(cls, keytype='rsa', size=None, passphrase=None, comment=None):
|
||||
"""Returns an Openssh_Keypair object generated using the supplied parameters or defaults to a RSA-2048 key
|
||||
|
||||
:keytype: One of rsa, dsa, ecdsa, ed25519
|
||||
:size: The key length for newly generated keys
|
||||
:passphrase: Secret of type Bytes used to encrypt the newly generated private key
|
||||
:comment: Comment for a newly generated OpenSSH public key
|
||||
"""
|
||||
|
||||
if comment is None:
|
||||
comment = "%s@%s" % (getuser(), gethostname())
|
||||
|
||||
asym_keypair = AsymmetricKeypair.generate(keytype, size, passphrase)
|
||||
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
|
||||
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
|
||||
fingerprint = calculate_fingerprint(openssh_publickey)
|
||||
|
||||
return cls(
|
||||
asym_keypair=asym_keypair,
|
||||
openssh_privatekey=openssh_privatekey,
|
||||
openssh_publickey=openssh_publickey,
|
||||
fingerprint=fingerprint,
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, passphrase=None, no_public_key=False):
|
||||
"""Returns an Openssh_Keypair object loaded from the supplied file path
|
||||
|
||||
:path: A path to an existing private key to be loaded
|
||||
:passphrase: Secret used to decrypt the private key being loaded
|
||||
:no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
|
||||
"""
|
||||
|
||||
if no_public_key:
|
||||
comment = ""
|
||||
else:
|
||||
comment = extract_comment(path + '.pub')
|
||||
|
||||
asym_keypair = AsymmetricKeypair.load(path, passphrase, 'SSH', 'SSH', no_public_key)
|
||||
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
|
||||
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
|
||||
fingerprint = calculate_fingerprint(openssh_publickey)
|
||||
|
||||
return cls(
|
||||
asym_keypair=asym_keypair,
|
||||
openssh_privatekey=openssh_privatekey,
|
||||
openssh_publickey=openssh_publickey,
|
||||
fingerprint=fingerprint,
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def encode_openssh_privatekey(asym_keypair, key_format):
|
||||
"""Returns an OpenSSH encoded private key for a given keypair
|
||||
|
||||
:asym_keypair: Asymmetric_Keypair from the private key is extracted
|
||||
:key_format: Format of the encoded private key.
|
||||
"""
|
||||
|
||||
if key_format == 'SSH':
|
||||
# Default to PEM format if SSH not available
|
||||
if not HAS_OPENSSH_PRIVATE_FORMAT:
|
||||
privatekey_format = serialization.PrivateFormat.PKCS8
|
||||
else:
|
||||
privatekey_format = serialization.PrivateFormat.OpenSSH
|
||||
elif key_format == 'PKCS8':
|
||||
privatekey_format = serialization.PrivateFormat.PKCS8
|
||||
elif key_format == 'PKCS1':
|
||||
if asym_keypair.key_type == 'ed25519':
|
||||
raise InvalidKeyFormatError("ed25519 keys cannot be represented in PKCS1 format")
|
||||
privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL
|
||||
else:
|
||||
raise InvalidKeyFormatError("The accepted private key formats are SSH, PKCS8, and PKCS1")
|
||||
|
||||
encoded_privatekey = asym_keypair.private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=privatekey_format,
|
||||
encryption_algorithm=asym_keypair.encryption_algorithm
|
||||
)
|
||||
|
||||
return encoded_privatekey
|
||||
|
||||
@staticmethod
|
||||
def encode_openssh_publickey(asym_keypair, comment):
|
||||
"""Returns an OpenSSH encoded public key for a given keypair
|
||||
|
||||
:asym_keypair: Asymmetric_Keypair from the public key is extracted
|
||||
:comment: Comment to apply to the end of the returned OpenSSH encoded public key
|
||||
"""
|
||||
encoded_publickey = asym_keypair.public_key.public_bytes(
|
||||
encoding=serialization.Encoding.OpenSSH,
|
||||
format=serialization.PublicFormat.OpenSSH,
|
||||
)
|
||||
|
||||
validate_comment(comment)
|
||||
|
||||
encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b''
|
||||
|
||||
return encoded_publickey
|
||||
|
||||
def __init__(self, asym_keypair, openssh_privatekey, openssh_publickey, fingerprint, comment):
|
||||
"""
|
||||
:asym_keypair: An Asymmetric_Keypair object from which the OpenSSH encoded keypair is derived
|
||||
:openssh_privatekey: An OpenSSH encoded private key
|
||||
:openssh_privatekey: An OpenSSH encoded public key
|
||||
:fingerprint: The fingerprint of the OpenSSH encoded public key of this keypair
|
||||
:comment: Comment applied to the OpenSSH public key of this keypair
|
||||
"""
|
||||
|
||||
self.__asym_keypair = asym_keypair
|
||||
self.__openssh_privatekey = openssh_privatekey
|
||||
self.__openssh_publickey = openssh_publickey
|
||||
self.__fingerprint = fingerprint
|
||||
self.__comment = comment
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, OpensshKeypair):
|
||||
return NotImplemented
|
||||
|
||||
return self.asymmetric_keypair == other.asymmetric_keypair and self.comment == other.comment
|
||||
|
||||
@property
|
||||
def asymmetric_keypair(self):
|
||||
"""Returns the underlying asymmetric key pair of this OpenSSH encoded key pair"""
|
||||
|
||||
return self.__asym_keypair
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
"""Returns the OpenSSH formatted private key of this key pair"""
|
||||
|
||||
return self.__openssh_privatekey
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
"""Returns the OpenSSH formatted public key of this key pair"""
|
||||
|
||||
return self.__openssh_publickey
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""Returns the size of the private key of this key pair"""
|
||||
|
||||
return self.__asym_keypair.size
|
||||
|
||||
@property
|
||||
def key_type(self):
|
||||
"""Returns the key type of this key pair"""
|
||||
|
||||
return self.__asym_keypair.key_type
|
||||
|
||||
@property
|
||||
def fingerprint(self):
|
||||
"""Returns the fingerprint (SHA256 Hash) of the public key of this key pair"""
|
||||
|
||||
return self.__fingerprint
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
"""Returns the comment applied to the OpenSSH formatted public key of this key pair"""
|
||||
|
||||
return self.__comment
|
||||
|
||||
@comment.setter
|
||||
def comment(self, comment):
|
||||
"""Updates the comment applied to the OpenSSH formatted public key of this key pair
|
||||
|
||||
:comment: Text to update the OpenSSH public key comment
|
||||
"""
|
||||
|
||||
validate_comment(comment)
|
||||
|
||||
self.__comment = comment
|
||||
encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) if self.__comment else b''
|
||||
self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + encoded_comment
|
||||
return self.__openssh_publickey
|
||||
|
||||
def update_passphrase(self, passphrase):
|
||||
"""Updates the passphrase used to encrypt the private key of this keypair
|
||||
|
||||
:passphrase: Text secret used for encryption
|
||||
"""
|
||||
|
||||
self.__asym_keypair.update_passphrase(passphrase)
|
||||
self.__openssh_privatekey = OpensshKeypair.encode_openssh_privatekey(self.__asym_keypair, 'SSH')
|
||||
|
||||
|
||||
def load_privatekey(path, passphrase, key_format):
|
||||
privatekey_loaders = {
|
||||
'PEM': serialization.load_pem_private_key,
|
||||
'DER': serialization.load_der_private_key,
|
||||
}
|
||||
|
||||
# OpenSSH formatted private keys are not available in Cryptography <3.0
|
||||
if hasattr(serialization, 'load_ssh_private_key'):
|
||||
privatekey_loaders['SSH'] = serialization.load_ssh_private_key
|
||||
else:
|
||||
privatekey_loaders['SSH'] = serialization.load_pem_private_key
|
||||
|
||||
try:
|
||||
privatekey_loader = privatekey_loaders[key_format]
|
||||
except KeyError:
|
||||
raise InvalidKeyFormatError(
|
||||
"%s is not a valid key format (%s)" % (
|
||||
key_format,
|
||||
','.join(privatekey_loaders.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise InvalidPrivateKeyFileError("No file was found at %s" % path)
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
privatekey = privatekey_loader(
|
||||
data=content,
|
||||
password=passphrase,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Revert to PEM if key could not be loaded in SSH format
|
||||
if key_format == 'SSH':
|
||||
try:
|
||||
privatekey = privatekey_loaders['PEM'](
|
||||
data=content,
|
||||
password=passphrase,
|
||||
backend=backend,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidPrivateKeyFileError(e)
|
||||
except TypeError as e:
|
||||
raise InvalidPassphraseError(e)
|
||||
except UnsupportedAlgorithm as e:
|
||||
raise InvalidAlgorithmError(e)
|
||||
else:
|
||||
raise InvalidPrivateKeyFileError(e)
|
||||
except TypeError as e:
|
||||
raise InvalidPassphraseError(e)
|
||||
except UnsupportedAlgorithm as e:
|
||||
raise InvalidAlgorithmError(e)
|
||||
|
||||
return privatekey
|
||||
|
||||
|
||||
def load_publickey(path, key_format):
|
||||
publickey_loaders = {
|
||||
'PEM': serialization.load_pem_public_key,
|
||||
'DER': serialization.load_der_public_key,
|
||||
'SSH': serialization.load_ssh_public_key,
|
||||
}
|
||||
|
||||
try:
|
||||
publickey_loader = publickey_loaders[key_format]
|
||||
except KeyError:
|
||||
raise InvalidKeyFormatError(
|
||||
"%s is not a valid key format (%s)" % (
|
||||
key_format,
|
||||
','.join(publickey_loaders.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise InvalidPublicKeyFileError("No file was found at %s" % path)
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
publickey = publickey_loader(
|
||||
data=content,
|
||||
backend=backend,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidPublicKeyFileError(e)
|
||||
except UnsupportedAlgorithm as e:
|
||||
raise InvalidAlgorithmError(e)
|
||||
|
||||
return publickey
|
||||
|
||||
|
||||
def compare_publickeys(pk1, pk2):
|
||||
a = isinstance(pk1, Ed25519PublicKey)
|
||||
b = isinstance(pk2, Ed25519PublicKey)
|
||||
if a or b:
|
||||
if not a or not b:
|
||||
return False
|
||||
a = pk1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
b = pk2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
return a == b
|
||||
else:
|
||||
return pk1.public_numbers() == pk2.public_numbers()
|
||||
|
||||
|
||||
def compare_encryption_algorithms(ea1, ea2):
|
||||
if isinstance(ea1, serialization.NoEncryption) and isinstance(ea2, serialization.NoEncryption):
|
||||
return True
|
||||
elif (isinstance(ea1, serialization.BestAvailableEncryption) and
|
||||
isinstance(ea2, serialization.BestAvailableEncryption)):
|
||||
return ea1.password == ea2.password
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_encryption_algorithm(passphrase):
|
||||
try:
|
||||
return serialization.BestAvailableEncryption(passphrase)
|
||||
except ValueError as e:
|
||||
raise InvalidPassphraseError(e)
|
||||
|
||||
|
||||
def validate_comment(comment):
|
||||
if not hasattr(comment, 'encode'):
|
||||
raise InvalidCommentError("%s cannot be encoded to text" % comment)
|
||||
|
||||
|
||||
def extract_comment(path):
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise InvalidPublicKeyFileError("No file was found at %s" % path)
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
fields = f.read().split(b' ', 2)
|
||||
if len(fields) == 3:
|
||||
comment = fields[2].decode(_TEXT_ENCODING)
|
||||
else:
|
||||
comment = ""
|
||||
except (IOError, OSError) as e:
|
||||
raise InvalidPublicKeyFileError(e)
|
||||
|
||||
return comment
|
||||
|
||||
|
||||
def calculate_fingerprint(openssh_publickey):
|
||||
digest = hashes.Hash(hashes.SHA256(), backend=backend)
|
||||
decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1])
|
||||
digest.update(decoded_pubkey)
|
||||
|
||||
return 'SHA256:%s' % b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=')
|
||||
403
plugins/module_utils/openssh/utils.py
Normal file
403
plugins/module_utils/openssh/utils.py
Normal file
@@ -0,0 +1,403 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from struct import Struct
|
||||
|
||||
from ansible.module_utils.six import PY3
|
||||
|
||||
# Protocol References
|
||||
# -------------------
|
||||
# https://datatracker.ietf.org/doc/html/rfc4251
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253
|
||||
# https://datatracker.ietf.org/doc/html/rfc5656
|
||||
# https://datatracker.ietf.org/doc/html/rfc8032
|
||||
#
|
||||
# Inspired by:
|
||||
# ------------
|
||||
# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
|
||||
# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
|
||||
|
||||
if PY3:
|
||||
long = int
|
||||
|
||||
# 0 (False) or 1 (True) encoded as a single byte
|
||||
_BOOLEAN = Struct(b'?')
|
||||
# Unsigned 8-bit integer in network-byte-order
|
||||
_UBYTE = Struct(b'!B')
|
||||
_UBYTE_MAX = 0xFF
|
||||
# Unsigned 32-bit integer in network-byte-order
|
||||
_UINT32 = Struct(b'!I')
|
||||
# Unsigned 32-bit little endian integer
|
||||
_UINT32_LE = Struct(b'<I')
|
||||
_UINT32_MAX = 0xFFFFFFFF
|
||||
# Unsigned 64-bit integer in network-byte-order
|
||||
_UINT64 = Struct(b'!Q')
|
||||
_UINT64_MAX = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
def any_in(sequence, *elements):
|
||||
return any(e in sequence for e in elements)
|
||||
|
||||
|
||||
def file_mode(path):
|
||||
if not os.path.exists(path):
|
||||
return 0o000
|
||||
return os.stat(path).st_mode & 0o777
|
||||
|
||||
|
||||
def parse_openssh_version(version_string):
|
||||
"""Parse the version output of ssh -V and return version numbers that can be compared"""
|
||||
|
||||
parsed_result = re.match(
|
||||
r"^.*openssh_(?P<version>[0-9.]+)(p?[0-9]+)[^0-9]*.*$", version_string.lower()
|
||||
)
|
||||
if parsed_result is not None:
|
||||
version = parsed_result.group("version").strip()
|
||||
else:
|
||||
version = None
|
||||
|
||||
return version
|
||||
|
||||
|
||||
@contextmanager
|
||||
def secure_open(path, mode):
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
||||
try:
|
||||
yield fd
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def secure_write(path, mode, content):
|
||||
with secure_open(path, mode) as fd:
|
||||
os.write(fd, content)
|
||||
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types
|
||||
class OpensshParser(object):
|
||||
"""Parser for OpenSSH encoded objects"""
|
||||
BOOLEAN_OFFSET = 1
|
||||
UINT32_OFFSET = 4
|
||||
UINT64_OFFSET = 8
|
||||
|
||||
def __init__(self, data):
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
raise TypeError("Data must be bytes-like not %s" % type(data))
|
||||
|
||||
self._data = memoryview(data) if PY3 else data
|
||||
self._pos = 0
|
||||
|
||||
def boolean(self):
|
||||
next_pos = self._check_position(self.BOOLEAN_OFFSET)
|
||||
|
||||
value = _BOOLEAN.unpack(self._data[self._pos:next_pos])[0]
|
||||
self._pos = next_pos
|
||||
return value
|
||||
|
||||
def uint32(self):
|
||||
next_pos = self._check_position(self.UINT32_OFFSET)
|
||||
|
||||
value = _UINT32.unpack(self._data[self._pos:next_pos])[0]
|
||||
self._pos = next_pos
|
||||
return value
|
||||
|
||||
def uint64(self):
|
||||
next_pos = self._check_position(self.UINT64_OFFSET)
|
||||
|
||||
value = _UINT64.unpack(self._data[self._pos:next_pos])[0]
|
||||
self._pos = next_pos
|
||||
return value
|
||||
|
||||
def string(self):
|
||||
length = self.uint32()
|
||||
|
||||
next_pos = self._check_position(length)
|
||||
|
||||
value = self._data[self._pos:next_pos]
|
||||
self._pos = next_pos
|
||||
# Cast to bytes is required as a memoryview slice is itself a memoryview
|
||||
return value if not PY3 else bytes(value)
|
||||
|
||||
def mpint(self):
|
||||
return self._big_int(self.string(), "big", signed=True)
|
||||
|
||||
def name_list(self):
|
||||
raw_string = self.string()
|
||||
return raw_string.decode('ASCII').split(',')
|
||||
|
||||
# Convenience function, but not an official data type from SSH
|
||||
def string_list(self):
|
||||
result = []
|
||||
raw_string = self.string()
|
||||
|
||||
if raw_string:
|
||||
parser = OpensshParser(raw_string)
|
||||
while parser.remaining_bytes():
|
||||
result.append(parser.string())
|
||||
|
||||
return result
|
||||
|
||||
# Convenience function, but not an official data type from SSH
|
||||
def option_list(self):
|
||||
result = []
|
||||
raw_string = self.string()
|
||||
|
||||
if raw_string:
|
||||
parser = OpensshParser(raw_string)
|
||||
|
||||
while parser.remaining_bytes():
|
||||
name = parser.string()
|
||||
data = parser.string()
|
||||
if data:
|
||||
# data is doubly-encoded
|
||||
data = OpensshParser(data).string()
|
||||
result.append((name, data))
|
||||
|
||||
return result
|
||||
|
||||
def seek(self, offset):
|
||||
self._pos = self._check_position(offset)
|
||||
|
||||
return self._pos
|
||||
|
||||
def remaining_bytes(self):
|
||||
return len(self._data) - self._pos
|
||||
|
||||
def _check_position(self, offset):
|
||||
if self._pos + offset > len(self._data):
|
||||
raise ValueError("Insufficient data remaining at position: %s" % self._pos)
|
||||
elif self._pos + offset < 0:
|
||||
raise ValueError("Position cannot be less than zero.")
|
||||
else:
|
||||
return self._pos + offset
|
||||
|
||||
@classmethod
|
||||
def signature_data(cls, signature_string):
|
||||
signature_data = {}
|
||||
|
||||
parser = cls(signature_string)
|
||||
signature_type = parser.string()
|
||||
signature_blob = parser.string()
|
||||
|
||||
blob_parser = cls(signature_blob)
|
||||
if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'):
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
# https://datatracker.ietf.org/doc/html/rfc8332#section-3
|
||||
signature_data['s'] = cls._big_int(signature_blob, "big")
|
||||
elif signature_type == b'ssh-dss':
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
signature_data['r'] = cls._big_int(signature_blob[:20], "big")
|
||||
signature_data['s'] = cls._big_int(signature_blob[20:], "big")
|
||||
elif signature_type in (b'ecdsa-sha2-nistp256', b'ecdsa-sha2-nistp384', b'ecdsa-sha2-nistp521'):
|
||||
# https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
|
||||
signature_data['r'] = blob_parser.mpint()
|
||||
signature_data['s'] = blob_parser.mpint()
|
||||
elif signature_type == b'ssh-ed25519':
|
||||
# https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2
|
||||
signature_data['R'] = cls._big_int(signature_blob[:32], "little")
|
||||
signature_data['S'] = cls._big_int(signature_blob[32:], "little")
|
||||
else:
|
||||
raise ValueError("%s is not a valid signature type" % signature_type)
|
||||
|
||||
signature_data['signature_type'] = signature_type
|
||||
|
||||
return signature_data
|
||||
|
||||
@classmethod
|
||||
def _big_int(cls, raw_string, byte_order, signed=False):
|
||||
if byte_order not in ("big", "little"):
|
||||
raise ValueError("Byte_order must be one of (big, little) not %s" % byte_order)
|
||||
|
||||
if PY3:
|
||||
return int.from_bytes(raw_string, byte_order, signed=signed)
|
||||
|
||||
result = 0
|
||||
byte_length = len(raw_string)
|
||||
|
||||
if byte_length > 0:
|
||||
# Check sign-bit
|
||||
msb = raw_string[0] if byte_order == "big" else raw_string[-1]
|
||||
negative = bool(ord(msb) & 0x80)
|
||||
# Match pad value for two's complement
|
||||
pad = b'\xFF' if signed and negative else b'\x00'
|
||||
# The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back
|
||||
pad_length = (4 - byte_length % 4)
|
||||
if pad_length < 4:
|
||||
raw_string = pad * pad_length + raw_string if byte_order == "big" else raw_string + pad * pad_length
|
||||
byte_length += pad_length
|
||||
# Accumulate arbitrary precision integer bytes in the appropriate order
|
||||
if byte_order == "big":
|
||||
for i in range(0, byte_length, cls.UINT32_OFFSET):
|
||||
left_shift = result << cls.UINT32_OFFSET * 8
|
||||
result = left_shift + _UINT32.unpack(raw_string[i:i + cls.UINT32_OFFSET])[0]
|
||||
else:
|
||||
for i in range(byte_length, 0, -cls.UINT32_OFFSET):
|
||||
left_shift = result << cls.UINT32_OFFSET * 8
|
||||
result = left_shift + _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET:i])[0]
|
||||
# Adjust for two's complement
|
||||
if signed and negative:
|
||||
result -= 1 << (8 * byte_length)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _OpensshWriter(object):
|
||||
"""Writes SSH encoded values to a bytes-like buffer
|
||||
|
||||
.. warning::
|
||||
This class is a private API and must not be exported outside of the openssh module_utils.
|
||||
It is not to be used to construct Openssh objects, but rather as a utility to assist
|
||||
in validating parsed material.
|
||||
"""
|
||||
def __init__(self, buffer=None):
|
||||
if buffer is not None:
|
||||
if not isinstance(buffer, (bytes, bytearray)):
|
||||
raise TypeError("Buffer must be a bytes-like object not %s" % type(buffer))
|
||||
else:
|
||||
buffer = bytearray()
|
||||
|
||||
self._buff = buffer
|
||||
|
||||
def boolean(self, value):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError("Value must be of type bool not %s" % type(value))
|
||||
|
||||
self._buff.extend(_BOOLEAN.pack(value))
|
||||
|
||||
return self
|
||||
|
||||
def uint32(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError("Value must be of type int not %s" % type(value))
|
||||
if value < 0 or value > _UINT32_MAX:
|
||||
raise ValueError("Value must be a positive integer less than %s" % _UINT32_MAX)
|
||||
|
||||
self._buff.extend(_UINT32.pack(value))
|
||||
|
||||
return self
|
||||
|
||||
def uint64(self, value):
|
||||
if not isinstance(value, (long, int)):
|
||||
raise TypeError("Value must be of type (long, int) not %s" % type(value))
|
||||
if value < 0 or value > _UINT64_MAX:
|
||||
raise ValueError("Value must be a positive integer less than %s" % _UINT64_MAX)
|
||||
|
||||
self._buff.extend(_UINT64.pack(value))
|
||||
|
||||
return self
|
||||
|
||||
def string(self, value):
|
||||
if not isinstance(value, (bytes, bytearray)):
|
||||
raise TypeError("Value must be bytes-like not %s" % type(value))
|
||||
self.uint32(len(value))
|
||||
self._buff.extend(value)
|
||||
|
||||
return self
|
||||
|
||||
def mpint(self, value):
|
||||
if not isinstance(value, (int, long)):
|
||||
raise TypeError("Value must be of type (long, int) not %s" % type(value))
|
||||
|
||||
self.string(self._int_to_mpint(value))
|
||||
|
||||
return self
|
||||
|
||||
def name_list(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("Value must be a list of byte strings not %s" % type(value))
|
||||
|
||||
try:
|
||||
self.string(','.join(value).encode('ASCII'))
|
||||
except UnicodeEncodeError as e:
|
||||
raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e)
|
||||
|
||||
return self
|
||||
|
||||
def string_list(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("Value must be a list of byte string not %s" % type(value))
|
||||
|
||||
writer = _OpensshWriter()
|
||||
for s in value:
|
||||
writer.string(s)
|
||||
|
||||
self.string(writer.bytes())
|
||||
|
||||
return self
|
||||
|
||||
def option_list(self, value):
|
||||
if not isinstance(value, list) or (value and not isinstance(value[0], tuple)):
|
||||
raise TypeError("Value must be a list of tuples")
|
||||
|
||||
writer = _OpensshWriter()
|
||||
for name, data in value:
|
||||
writer.string(name)
|
||||
# SSH option data is encoded twice though this behavior is not documented
|
||||
writer.string(_OpensshWriter().string(data).bytes() if data else bytes())
|
||||
|
||||
self.string(writer.bytes())
|
||||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _int_to_mpint(num):
|
||||
if PY3:
|
||||
byte_length = (num.bit_length() + 7) // 8
|
||||
try:
|
||||
result = num.to_bytes(byte_length, "big", signed=True)
|
||||
# Handles values which require \x00 or \xFF to pad sign-bit
|
||||
except OverflowError:
|
||||
result = num.to_bytes(byte_length + 1, "big", signed=True)
|
||||
else:
|
||||
result = bytes()
|
||||
# 0 and -1 are treated as special cases since they are used as sentinels for all other values
|
||||
if num == 0:
|
||||
result += b'\x00'
|
||||
elif num == -1:
|
||||
result += b'\xFF'
|
||||
elif num > 0:
|
||||
while num >> 32:
|
||||
result = _UINT32.pack(num & _UINT32_MAX) + result
|
||||
num = num >> 32
|
||||
# Pack last 4 bytes individually to discard insignificant bytes
|
||||
while num:
|
||||
result = _UBYTE.pack(num & _UBYTE_MAX) + result
|
||||
num = num >> 8
|
||||
# Zero pad final byte if most-significant bit is 1 as per mpint definition
|
||||
if ord(result[0]) & 0x80:
|
||||
result = b'\x00' + result
|
||||
else:
|
||||
while (num >> 32) < -1:
|
||||
result = _UINT32.pack(num & _UINT32_MAX) + result
|
||||
num = num >> 32
|
||||
while num < -1:
|
||||
result = _UBYTE.pack(num & _UBYTE_MAX) + result
|
||||
num = num >> 8
|
||||
if not ord(result[0]) & 0x80:
|
||||
result = b'\xFF' + result
|
||||
|
||||
return result
|
||||
|
||||
def bytes(self):
|
||||
return bytes(self._buff)
|
||||
17
plugins/module_utils/version.py
Normal file
17
plugins/module_utils/version.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
"""Provide version object to compare version numbers."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
|
||||
# remove the _version.py file, and replace the following import by
|
||||
#
|
||||
# from ansible.module_utils.compat.version import LooseVersion
|
||||
|
||||
from ._version import LooseVersion
|
||||
@@ -31,6 +31,8 @@ seealso:
|
||||
description: Retrieves facts about an ACME account.
|
||||
- module: community.crypto.openssl_privatekey
|
||||
description: Can be used to create a private account key.
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
description: Can be used to create a private account key without writing it to disk.
|
||||
- module: community.crypto.acme_inspect
|
||||
description: Allows to debug problems.
|
||||
extends_documentation_fragment:
|
||||
@@ -86,6 +88,39 @@ options:
|
||||
- "Mutually exclusive with C(new_account_key_src)."
|
||||
- "Required if C(new_account_key_src) is not used and state is C(changed_key)."
|
||||
type: str
|
||||
new_account_key_passphrase:
|
||||
description:
|
||||
- Phassphrase to use to decode the new 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
|
||||
external_account_binding:
|
||||
description:
|
||||
- Allows to provide external account binding data during account creation.
|
||||
- This is used by CAs like Sectigo to bind a new ACME account to an existing CA-specific
|
||||
account, to be able to properly identify a customer.
|
||||
- Only used when creating a new account. Can not be specified for ACME v1.
|
||||
type: dict
|
||||
suboptions:
|
||||
kid:
|
||||
description:
|
||||
- The key identifier provided by the CA.
|
||||
type: str
|
||||
required: true
|
||||
alg:
|
||||
description:
|
||||
- The MAC algorithm provided by the CA.
|
||||
- If not specified by the CA, this is probably C(HS256).
|
||||
type: str
|
||||
required: true
|
||||
choices: [ HS256, HS384, HS512 ]
|
||||
key:
|
||||
description:
|
||||
- Base64 URL encoded value of the MAC key provided by the CA.
|
||||
- Padding (C(=) symbols at the end) can be omitted.
|
||||
type: str
|
||||
required: true
|
||||
version_added: 1.1.0
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
@@ -125,13 +160,23 @@ account_uri:
|
||||
type: str
|
||||
'''
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
ACMEAccount,
|
||||
handle_standard_module_arguments,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
create_backend,
|
||||
get_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,
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
|
||||
@@ -144,6 +189,12 @@ def main():
|
||||
contact=dict(type='list', elements='str', default=[]),
|
||||
new_account_key_src=dict(type='path'),
|
||||
new_account_key_content=dict(type='str', no_log=True),
|
||||
new_account_key_passphrase=dict(type='str', no_log=True),
|
||||
external_account_binding=dict(type='dict', options=dict(
|
||||
kid=dict(type='str', required=True),
|
||||
alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
|
||||
key=dict(type='str', required=True, no_log=True),
|
||||
))
|
||||
))
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
@@ -161,10 +212,23 @@ def main():
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
handle_standard_module_arguments(module, needs_acme_v2=True)
|
||||
backend = create_backend(module, True)
|
||||
|
||||
if module.params['external_account_binding']:
|
||||
# Make sure padding is there
|
||||
key = module.params['external_account_binding']['key']
|
||||
if len(key) % 4 != 0:
|
||||
key = key + ('=' * (4 - (len(key) % 4)))
|
||||
# Make sure key is Base64 encoded
|
||||
try:
|
||||
base64.urlsafe_b64decode(key)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e)
|
||||
module.params['external_account_binding']['key'] = key
|
||||
|
||||
try:
|
||||
account = ACMEAccount(module)
|
||||
client = ACMEClient(module, backend)
|
||||
account = ACMEAccount(client)
|
||||
changed = False
|
||||
state = module.params.get('state')
|
||||
diff_before = {}
|
||||
@@ -173,7 +237,7 @@ def main():
|
||||
created, account_data = account.setup_account(allow_creation=False)
|
||||
if account_data:
|
||||
diff_before = dict(account_data)
|
||||
diff_before['public_account_key'] = account.key_data['jwk']
|
||||
diff_before['public_account_key'] = client.account_key_data['jwk']
|
||||
if created:
|
||||
raise AssertionError('Unwanted account creation')
|
||||
if account_data is not None:
|
||||
@@ -183,19 +247,19 @@ def main():
|
||||
payload = {
|
||||
'status': 'deactivated'
|
||||
}
|
||||
result, info = account.send_signed_request(account.uri, payload)
|
||||
if info['status'] != 200:
|
||||
raise ModuleFailException('Error deactivating account: {0} {1}'.format(info['status'], result))
|
||||
result, info = client.send_signed_request(
|
||||
client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200])
|
||||
changed = True
|
||||
elif state == 'present':
|
||||
allow_creation = module.params.get('allow_creation')
|
||||
# Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us)
|
||||
contact = [str(v) for v in module.params.get('contact')]
|
||||
terms_agreed = module.params.get('terms_agreed')
|
||||
external_account_binding = module.params.get('external_account_binding')
|
||||
created, account_data = account.setup_account(
|
||||
contact,
|
||||
terms_agreed=terms_agreed,
|
||||
allow_creation=allow_creation,
|
||||
external_account_binding=external_account_binding,
|
||||
)
|
||||
if account_data is None:
|
||||
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
||||
@@ -203,21 +267,23 @@ def main():
|
||||
diff_before = {}
|
||||
else:
|
||||
diff_before = dict(account_data)
|
||||
diff_before['public_account_key'] = account.key_data['jwk']
|
||||
diff_before['public_account_key'] = client.account_key_data['jwk']
|
||||
updated = False
|
||||
if not created:
|
||||
updated, account_data = account.update_account(account_data, contact)
|
||||
changed = created or updated
|
||||
diff_after = dict(account_data)
|
||||
diff_after['public_account_key'] = account.key_data['jwk']
|
||||
diff_after['public_account_key'] = client.account_key_data['jwk']
|
||||
elif state == 'changed_key':
|
||||
# Parse new account key
|
||||
error, new_key_data = account.parse_key(
|
||||
module.params.get('new_account_key_src'),
|
||||
module.params.get('new_account_key_content')
|
||||
)
|
||||
if error:
|
||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||
try:
|
||||
new_key_data = client.parse_key(
|
||||
module.params.get('new_account_key_src'),
|
||||
module.params.get('new_account_key_content'),
|
||||
passphrase=module.params.get('new_account_key_passphrase'),
|
||||
)
|
||||
except KeyParsingError as e:
|
||||
raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg))
|
||||
# Verify that the account exists and has not been deactivated
|
||||
created, account_data = account.setup_account(allow_creation=False)
|
||||
if created:
|
||||
@@ -225,30 +291,29 @@ def main():
|
||||
if account_data is None:
|
||||
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
||||
diff_before = dict(account_data)
|
||||
diff_before['public_account_key'] = account.key_data['jwk']
|
||||
diff_before['public_account_key'] = client.account_key_data['jwk']
|
||||
# Now we can start the account key rollover
|
||||
if not module.check_mode:
|
||||
# Compose inner signed message
|
||||
# https://tools.ietf.org/html/rfc8555#section-7.3.5
|
||||
url = account.directory['keyChange']
|
||||
url = client.directory['keyChange']
|
||||
protected = {
|
||||
"alg": new_key_data['alg'],
|
||||
"jwk": new_key_data['jwk'],
|
||||
"url": url,
|
||||
}
|
||||
payload = {
|
||||
"account": account.uri,
|
||||
"account": client.account_uri,
|
||||
"newKey": new_key_data['jwk'], # specified in draft 12 and older
|
||||
"oldKey": account.jwk, # specified in draft 13 and newer
|
||||
"oldKey": client.account_jwk, # specified in draft 13 and newer
|
||||
}
|
||||
data = account.sign_request(protected, payload, new_key_data)
|
||||
data = client.sign_request(protected, payload, new_key_data)
|
||||
# Send request and verify result
|
||||
result, info = account.send_signed_request(url, data)
|
||||
if info['status'] != 200:
|
||||
raise ModuleFailException('Error account key rollover: {0} {1}'.format(info['status'], result))
|
||||
result, info = client.send_signed_request(
|
||||
url, data, error_msg='Failed to rollover account key', expected_status_codes=[200])
|
||||
if module._diff:
|
||||
account.key_data = new_key_data
|
||||
account.jws_header['alg'] = new_key_data['alg']
|
||||
client.account_key_data = new_key_data
|
||||
client.account_jws_header['alg'] = new_key_data['alg']
|
||||
diff_after = account.get_account_data()
|
||||
elif module._diff:
|
||||
# Kind of fake diff_after
|
||||
@@ -257,7 +322,7 @@ def main():
|
||||
changed = True
|
||||
result = {
|
||||
'changed': changed,
|
||||
'account_uri': account.uri,
|
||||
'account_uri': client.account_uri,
|
||||
}
|
||||
if module._diff:
|
||||
result['diff'] = {
|
||||
|
||||
@@ -23,12 +23,17 @@ notes:
|
||||
accounts."
|
||||
- "This module was called C(acme_account_facts) before Ansible 2.8. The usage
|
||||
did not change."
|
||||
- Supports C(check_mode).
|
||||
options:
|
||||
retrieve_orders:
|
||||
description:
|
||||
- "Whether to retrieve the list of order URLs or order objects, if provided
|
||||
by the ACME server."
|
||||
- "A value of C(ignore) will not fetch the list of orders."
|
||||
- "If the value is not C(ignore) and the ACME server supports orders, the C(order_uris)
|
||||
return value is always populated. The C(orders) return value currently depends on
|
||||
whether this option is set to C(url_list) or C(object_list). In community.crypto 2.0.0,
|
||||
it will only be returned if this option is set to C(object_list)."
|
||||
- "Currently, Let's Encrypt does not return orders, so the C(orders) result
|
||||
will always be empty."
|
||||
type: str
|
||||
@@ -49,27 +54,30 @@ EXAMPLES = '''
|
||||
- name: Check whether an account with the given account key exists
|
||||
community.crypto.acme_account_info:
|
||||
account_key_src: /etc/pki/cert/private/account.key
|
||||
register: account_data
|
||||
register: account_data
|
||||
- name: Verify that account exists
|
||||
assert:
|
||||
that:
|
||||
- account_data.exists
|
||||
- name: Print account URI
|
||||
debug: var=account_data.account_uri
|
||||
ansible.builtin.debug:
|
||||
var: account_data.account_uri
|
||||
- name: Print account contacts
|
||||
debug: var=account_data.account.contact
|
||||
ansible.builtin.debug:
|
||||
var: account_data.account.contact
|
||||
|
||||
- name: Check whether the account exists and is accessible with the given account key
|
||||
acme_account_info:
|
||||
account_key_content: "{{ acme_account_key }}"
|
||||
account_uri: "{{ acme_account_uri }}"
|
||||
register: account_data
|
||||
register: account_data
|
||||
- name: Verify that account exists
|
||||
assert:
|
||||
that:
|
||||
- account_data.exists
|
||||
- name: Print account contacts
|
||||
debug: var=account_data.account.contact
|
||||
ansible.builtin.debug:
|
||||
var: account_data.account.contact
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
@@ -93,7 +101,7 @@ account:
|
||||
returned: always
|
||||
type: list
|
||||
elements: str
|
||||
sample: "['mailto:me@example.com', 'tel:00123456789']"
|
||||
sample: ['mailto:me@example.com', 'tel:00123456789']
|
||||
status:
|
||||
description: the account's status
|
||||
returned: always
|
||||
@@ -117,7 +125,8 @@ account:
|
||||
orders:
|
||||
description:
|
||||
- "The list of orders."
|
||||
- "If I(retrieve_orders) is C(url_list), this will be a list of URLs."
|
||||
- "If I(retrieve_orders) is C(url_list), this will be a list of URLs. In community.crypto 2.0.0,
|
||||
this return value will no longer be returned for C(url_list)."
|
||||
- "If I(retrieve_orders) is C(object_list), this will be a list of objects."
|
||||
type: list
|
||||
#elements: ... depends on retrieve_orders
|
||||
@@ -190,27 +199,45 @@ orders:
|
||||
- The URL for retrieving the certificate.
|
||||
type: str
|
||||
returned: when certificate was issued
|
||||
|
||||
order_uris:
|
||||
description:
|
||||
- "The list of orders."
|
||||
- "If I(retrieve_orders) is C(url_list), this will be a list of URLs."
|
||||
- "If I(retrieve_orders) is C(object_list), this will be a list of objects."
|
||||
type: list
|
||||
elements: str
|
||||
returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing
|
||||
version_added: 1.5.0
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
ACMEAccount,
|
||||
handle_standard_module_arguments,
|
||||
process_links,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
create_backend,
|
||||
get_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.utils import (
|
||||
process_links,
|
||||
)
|
||||
|
||||
|
||||
def get_orders_list(module, account, orders_url):
|
||||
def get_orders_list(module, client, orders_url):
|
||||
'''
|
||||
Retrieves orders list (handles pagination).
|
||||
'''
|
||||
orders = []
|
||||
while orders_url:
|
||||
# Get part of orders list
|
||||
res, info = account.get_request(orders_url, parse_json_result=True, fail_on_error=True)
|
||||
res, info = client.get_request(orders_url, parse_json_result=True, fail_on_error=True)
|
||||
if not res.get('orders'):
|
||||
if orders:
|
||||
module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url))
|
||||
@@ -233,11 +260,11 @@ def get_orders_list(module, account, orders_url):
|
||||
return orders
|
||||
|
||||
|
||||
def get_order(account, order_url):
|
||||
def get_order(client, order_url):
|
||||
'''
|
||||
Retrieve order data.
|
||||
'''
|
||||
return account.get_request(order_url, parse_json_result=True, fail_on_error=True)[0]
|
||||
return client.get_request(order_url, parse_json_result=True, fail_on_error=True)[0]
|
||||
|
||||
|
||||
def main():
|
||||
@@ -258,10 +285,11 @@ def main():
|
||||
if module._name in ('acme_account_facts', 'community.crypto.acme_account_facts'):
|
||||
module.deprecate("The 'acme_account_facts' module has been renamed to 'acme_account_info'",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
handle_standard_module_arguments(module, needs_acme_v2=True)
|
||||
backend = create_backend(module, True)
|
||||
|
||||
try:
|
||||
account = ACMEAccount(module)
|
||||
client = ACMEClient(module, backend)
|
||||
account = ACMEAccount(client)
|
||||
# Check whether account exists
|
||||
created, account_data = account.setup_account(
|
||||
[],
|
||||
@@ -272,22 +300,28 @@ def main():
|
||||
raise AssertionError('Unwanted account creation')
|
||||
result = {
|
||||
'changed': False,
|
||||
'exists': account.uri is not None,
|
||||
'account_uri': account.uri,
|
||||
'exists': client.account_uri is not None,
|
||||
'account_uri': client.account_uri,
|
||||
}
|
||||
if account.uri is not None:
|
||||
if client.account_uri is not None:
|
||||
# Make sure promised data is there
|
||||
if 'contact' not in account_data:
|
||||
account_data['contact'] = []
|
||||
account_data['public_account_key'] = account.key_data['jwk']
|
||||
account_data['public_account_key'] = client.account_key_data['jwk']
|
||||
result['account'] = account_data
|
||||
# Retrieve orders list
|
||||
if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore':
|
||||
orders = get_orders_list(module, account, account_data['orders'])
|
||||
orders = get_orders_list(module, client, account_data['orders'])
|
||||
result['order_uris'] = orders
|
||||
if module.params['retrieve_orders'] == 'url_list':
|
||||
module.deprecate(
|
||||
'retrieve_orders=url_list now returns the order URI list as `order_uris`.'
|
||||
' Right now it also returns this list as `orders` for backwards compatibility,'
|
||||
' but this will stop in community.crypto 2.0.0',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
result['orders'] = orders
|
||||
else:
|
||||
result['orders'] = [get_order(account, order) for order in orders]
|
||||
if module.params['retrieve_orders'] == 'object_list':
|
||||
result['orders'] = [get_order(client, order) for order in orders]
|
||||
module.exit_json(**result)
|
||||
except ModuleFailException as e:
|
||||
e.do_fail(module)
|
||||
|
||||
@@ -62,9 +62,13 @@ seealso:
|
||||
description: Helps preparing C(tls-alpn-01) challenges.
|
||||
- module: community.crypto.openssl_privatekey
|
||||
description: Can be used to create private keys (both for certificates and accounts).
|
||||
- module: commuinty.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
description: Can be used to create private keys without writing it to disk (both for certificates and accounts).
|
||||
- module: community.crypto.openssl_csr
|
||||
description: Can be used to create a Certificate Signing Request (CSR).
|
||||
- module: comunity.crypto.certificate_complete_chain
|
||||
- module: community.crypto.openssl_csr_pipe
|
||||
description: Can be used to create a Certificate Signing Request (CSR) without writing it to disk.
|
||||
- module: community.crypto.certificate_complete_chain
|
||||
description: Allows to find the root certificate for the returned fullchain.
|
||||
- module: community.crypto.acme_certificate_revoke
|
||||
description: Allows to revoke certificates.
|
||||
@@ -117,7 +121,7 @@ options:
|
||||
csr:
|
||||
description:
|
||||
- "File containing the CSR for the new certificate."
|
||||
- "Can be created with C(openssl req ...)."
|
||||
- "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."
|
||||
@@ -125,9 +129,23 @@ options:
|
||||
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 I(csr) or I(csr_content) must be specified.
|
||||
type: path
|
||||
required: true
|
||||
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 I(csr) or I(csr_content) must be specified.
|
||||
type: str
|
||||
version_added: 1.2.0
|
||||
data:
|
||||
description:
|
||||
- "The data to validate ongoing challenges. This must be specified for
|
||||
@@ -220,11 +238,13 @@ options:
|
||||
- "Determines which certificates in the chain will be tested."
|
||||
- "I(all) tests all certificates in the chain (excluding the leaf, which is
|
||||
identical in all chains)."
|
||||
- "I(first) only tests the first certificate in the chain, i.e. the one which
|
||||
signed the leaf."
|
||||
- "I(last) only tests the last certificate in the chain, i.e. the one furthest
|
||||
away from the leaf. Its issuer is the root certificate of this chain."
|
||||
type: str
|
||||
default: all
|
||||
choices: [last, all]
|
||||
choices: [first, last, all]
|
||||
issuer:
|
||||
description:
|
||||
- "Allows to specify parts of the issuer of a certificate in the chain must
|
||||
@@ -277,7 +297,7 @@ EXAMPLES = r'''
|
||||
- name: Create a challenge for sample.com using a account key file.
|
||||
community.crypto.acme_certificate:
|
||||
account_key_src: /etc/pki/cert/private/account.key
|
||||
csr: /etc/pki/cert/csr/sample.com.csr
|
||||
csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
|
||||
dest: /etc/httpd/ssl/sample.com.crt
|
||||
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
|
||||
register: sample_com_challenge
|
||||
@@ -288,14 +308,14 @@ EXAMPLES = r'''
|
||||
# - copy:
|
||||
# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }}
|
||||
# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
|
||||
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
|
||||
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
|
||||
#
|
||||
# Alternative way:
|
||||
#
|
||||
# - copy:
|
||||
# dest: /var/www/{{ item.key }}/{{ item.value['http-01']['resource'] }}
|
||||
# content: "{{ item.value['http-01']['resource_value'] }}"
|
||||
# loop: "{{ sample_com_challenge.challenge_data | dictsort }}"
|
||||
# loop: "{{ sample_com_challenge.challenge_data | dict2items }}"
|
||||
# when: sample_com_challenge is changed
|
||||
|
||||
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
|
||||
@@ -347,7 +367,7 @@ EXAMPLES = r'''
|
||||
# # 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 }}"
|
||||
# loop: "{{ sample_com_challenge.challenge_data_dns | dictsort }}"
|
||||
# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
|
||||
# when: sample_com_challenge is changed
|
||||
|
||||
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
|
||||
@@ -447,7 +467,20 @@ authorizations:
|
||||
- Maps an identifier to ACME authorization objects. See U(https://tools.ietf.org/html/rfc8555#section-7.1.4).
|
||||
returned: changed
|
||||
type: dict
|
||||
sample: '{"example.com":{...}}'
|
||||
sample:
|
||||
example.com:
|
||||
identifier:
|
||||
type: dns
|
||||
value: example.com
|
||||
status: valid
|
||||
expires: '2022-08-04T01:02:03.45Z'
|
||||
challenges:
|
||||
- url: https://example.org/acme/challenge/12345
|
||||
type: http-01
|
||||
status: valid
|
||||
token: A5b1C3d2E9f8G7h6
|
||||
validated: '2022-08-01T01:01:02.34Z'
|
||||
wildcard: false
|
||||
order_uri:
|
||||
description: ACME order URI.
|
||||
returned: changed
|
||||
@@ -488,102 +521,81 @@ all_chains:
|
||||
returned: always
|
||||
'''
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from datetime import datetime
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes, to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
parse_name_field,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_name_to_oid,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
write_file,
|
||||
nopad_b64,
|
||||
pem_to_der,
|
||||
ACMEAccount,
|
||||
HAS_CURRENT_CRYPTOGRAPHY,
|
||||
cryptography_get_csr_identifiers,
|
||||
openssl_get_csr_identifiers,
|
||||
cryptography_get_cert_days,
|
||||
handle_standard_module_arguments,
|
||||
process_links,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
create_backend,
|
||||
get_default_argspec,
|
||||
ACMEClient,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.x509
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
|
||||
ACMEAccount,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
|
||||
combine_identifier,
|
||||
split_identifier,
|
||||
Authorization,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
|
||||
retrieve_acme_v1_certificate,
|
||||
CertificateChain,
|
||||
Criterium,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
ModuleFailException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
|
||||
write_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.orders import (
|
||||
Order,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
pem_to_der,
|
||||
)
|
||||
|
||||
|
||||
def get_cert_days(module, cert_file):
|
||||
'''
|
||||
Return the days the certificate in cert_file remains valid and -1
|
||||
if the file was not found. If cert_file contains more than one
|
||||
certificate, only the first one will be considered.
|
||||
'''
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return cryptography_get_cert_days(module, cert_file)
|
||||
if not os.path.exists(cert_file):
|
||||
return -1
|
||||
|
||||
openssl_bin = module.get_bin_path('openssl', True)
|
||||
openssl_cert_cmd = [openssl_bin, "x509", "-in", cert_file, "-noout", "-text"]
|
||||
dummy, out, dummy = module.run_command(openssl_cert_cmd, check_rc=True, encoding=None)
|
||||
try:
|
||||
not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", out.decode('utf8')).group(1)
|
||||
not_after = datetime.fromtimestamp(time.mktime(time.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z')))
|
||||
except AttributeError:
|
||||
raise ModuleFailException("No 'Not after' date found in {0}".format(cert_file))
|
||||
except ValueError:
|
||||
raise ModuleFailException("Failed to parse 'Not after' date of {0}".format(cert_file))
|
||||
now = datetime.utcnow()
|
||||
return (not_after - now).days
|
||||
|
||||
|
||||
class ACMEClient(object):
|
||||
class ACMECertificateClient(object):
|
||||
'''
|
||||
ACME client class. Uses an ACME account object and a CSR to
|
||||
start and validate ACME challenges and download the respective
|
||||
certificates.
|
||||
'''
|
||||
|
||||
def __init__(self, module):
|
||||
def __init__(self, module, backend):
|
||||
self.module = module
|
||||
self.version = module.params['acme_version']
|
||||
self.challenge = module.params['challenge']
|
||||
self.csr = module.params['csr']
|
||||
self.csr_content = module.params['csr_content']
|
||||
self.dest = module.params.get('dest')
|
||||
self.fullchain_dest = module.params.get('fullchain_dest')
|
||||
self.chain_dest = module.params.get('chain_dest')
|
||||
self.account = ACMEAccount(module)
|
||||
self.directory = self.account.directory
|
||||
self.client = ACMEClient(module, backend)
|
||||
self.account = ACMEAccount(self.client)
|
||||
self.directory = self.client.directory
|
||||
self.data = module.params['data']
|
||||
self.authorizations = None
|
||||
self.cert_days = -1
|
||||
self.order = None
|
||||
self.order_uri = self.data.get('order_uri') if self.data else None
|
||||
self.finalize_uri = None
|
||||
self.all_chains = None
|
||||
self.select_chain_matcher = []
|
||||
|
||||
if self.module.params['select_chain']:
|
||||
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
|
||||
self.select_chain_matcher.append(
|
||||
self.client.backend.create_chain_matcher(
|
||||
Criterium(criterium, index=criterium_idx)))
|
||||
|
||||
# Make sure account exists
|
||||
modify_account = module.params['modify_account']
|
||||
@@ -611,290 +623,11 @@ class ACMEClient(object):
|
||||
# signed ACME request.
|
||||
pass
|
||||
|
||||
if not os.path.exists(self.csr):
|
||||
if self.csr is not None and not os.path.exists(self.csr):
|
||||
raise ModuleFailException("CSR %s not found" % (self.csr))
|
||||
|
||||
self._openssl_bin = module.get_bin_path('openssl', True)
|
||||
|
||||
# Extract list of identifiers from CSR
|
||||
self.identifiers = self._get_csr_identifiers()
|
||||
|
||||
def _get_csr_identifiers(self):
|
||||
'''
|
||||
Parse the CSR and return the list of requested identifiers
|
||||
'''
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return cryptography_get_csr_identifiers(self.module, self.csr)
|
||||
else:
|
||||
return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr)
|
||||
|
||||
def _add_or_update_auth(self, identifier_type, identifier, auth):
|
||||
'''
|
||||
Add or update the given authorization in the global authorizations list.
|
||||
Return True if the auth was updated/added and False if no change was
|
||||
necessary.
|
||||
'''
|
||||
if self.authorizations.get(identifier_type + ':' + identifier) == auth:
|
||||
return False
|
||||
self.authorizations[identifier_type + ':' + identifier] = auth
|
||||
return True
|
||||
|
||||
def _new_authz_v1(self, identifier_type, identifier):
|
||||
'''
|
||||
Create a new authorization for the given identifier.
|
||||
Return the authorization object of the new authorization
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
|
||||
'''
|
||||
new_authz = {
|
||||
"resource": "new-authz",
|
||||
"identifier": {"type": identifier_type, "value": identifier},
|
||||
}
|
||||
|
||||
result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz)
|
||||
if info['status'] not in [200, 201]:
|
||||
raise ModuleFailException("Error requesting challenges: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
else:
|
||||
result['uri'] = info['location']
|
||||
return result
|
||||
|
||||
def _get_challenge_data(self, auth, identifier_type, identifier):
|
||||
'''
|
||||
Returns a dict with the data for all proposed (and supported) challenges
|
||||
of the given authorization.
|
||||
'''
|
||||
|
||||
data = {}
|
||||
# no need to choose a specific challenge here as this module
|
||||
# is not responsible for fulfilling the challenges. Calculate
|
||||
# and return the required information for each challenge.
|
||||
for challenge in auth['challenges']:
|
||||
challenge_type = challenge['type']
|
||||
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
|
||||
keyauthorization = self.account.get_keyauthorization(token)
|
||||
|
||||
if challenge_type == 'http-01':
|
||||
# https://tools.ietf.org/html/rfc8555#section-8.3
|
||||
resource = '.well-known/acme-challenge/' + token
|
||||
data[challenge_type] = {'resource': resource, 'resource_value': keyauthorization}
|
||||
elif challenge_type == 'dns-01':
|
||||
if identifier_type != 'dns':
|
||||
continue
|
||||
# https://tools.ietf.org/html/rfc8555#section-8.4
|
||||
resource = '_acme-challenge'
|
||||
value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest())
|
||||
record = (resource + identifier[1:]) if identifier.startswith('*.') else (resource + '.' + identifier)
|
||||
data[challenge_type] = {'resource': resource, 'resource_value': value, 'record': record}
|
||||
elif challenge_type == 'tls-alpn-01':
|
||||
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3
|
||||
if identifier_type == 'ip':
|
||||
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
|
||||
resource = compat_ipaddress.ip_address(identifier).reverse_pointer
|
||||
if not resource.endswith('.'):
|
||||
resource += '.'
|
||||
else:
|
||||
resource = identifier
|
||||
value = base64.b64encode(hashlib.sha256(to_bytes(keyauthorization)).digest())
|
||||
data[challenge_type] = {'resource': resource, 'resource_original': identifier_type + ':' + identifier, 'resource_value': value}
|
||||
else:
|
||||
continue
|
||||
|
||||
return data
|
||||
|
||||
def _fail_challenge(self, identifier_type, identifier, auth, error):
|
||||
'''
|
||||
Aborts with a specific error for a challenge.
|
||||
'''
|
||||
error_details = ''
|
||||
# multiple challenges could have failed at this point, gather error
|
||||
# details for all of them before failing
|
||||
for challenge in auth['challenges']:
|
||||
if challenge['status'] == 'invalid':
|
||||
error_details += ' CHALLENGE: {0}'.format(challenge['type'])
|
||||
if 'error' in challenge:
|
||||
error_details += ' DETAILS: {0};'.format(challenge['error']['detail'])
|
||||
else:
|
||||
error_details += ';'
|
||||
raise ModuleFailException("{0}: {1}".format(error.format(identifier_type + ':' + identifier), error_details))
|
||||
|
||||
def _validate_challenges(self, identifier_type, identifier, auth):
|
||||
'''
|
||||
Validate the authorization provided in the auth dict. Returns True
|
||||
when the validation was successful and False when it was not.
|
||||
'''
|
||||
for challenge in auth['challenges']:
|
||||
if self.challenge != challenge['type']:
|
||||
continue
|
||||
|
||||
uri = challenge['uri'] if self.version == 1 else challenge['url']
|
||||
|
||||
challenge_response = {}
|
||||
if self.version == 1:
|
||||
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
|
||||
keyauthorization = self.account.get_keyauthorization(token)
|
||||
challenge_response["resource"] = "challenge"
|
||||
challenge_response["keyAuthorization"] = keyauthorization
|
||||
challenge_response["type"] = self.challenge
|
||||
result, info = self.account.send_signed_request(uri, challenge_response)
|
||||
if info['status'] not in [200, 202]:
|
||||
raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
|
||||
status = ''
|
||||
|
||||
while status not in ['valid', 'invalid', 'revoked']:
|
||||
result, dummy = self.account.get_request(auth['uri'])
|
||||
result['uri'] = auth['uri']
|
||||
if self._add_or_update_auth(identifier_type, identifier, result):
|
||||
self.changed = True
|
||||
# https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
|
||||
# "status (required, string): ...
|
||||
# If this field is missing, then the default value is "pending"."
|
||||
if self.version == 1 and 'status' not in result:
|
||||
status = 'pending'
|
||||
else:
|
||||
status = result['status']
|
||||
time.sleep(2)
|
||||
|
||||
if status == 'invalid':
|
||||
self._fail_challenge(identifier_type, identifier, result, 'Authorization for {0} returned invalid')
|
||||
|
||||
return status == 'valid'
|
||||
|
||||
def _finalize_cert(self):
|
||||
'''
|
||||
Create a new certificate based on the csr.
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
'''
|
||||
csr = pem_to_der(self.csr)
|
||||
new_cert = {
|
||||
"csr": nopad_b64(csr),
|
||||
}
|
||||
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
||||
if info['status'] not in [200]:
|
||||
raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
|
||||
status = result['status']
|
||||
while status not in ['valid', 'invalid']:
|
||||
time.sleep(2)
|
||||
result, dummy = self.account.get_request(self.order_uri)
|
||||
status = result['status']
|
||||
|
||||
if status != 'valid':
|
||||
raise ModuleFailException("Error new cert: CODE: {0} STATUS: {1} RESULT: {2}".format(info['status'], status, result))
|
||||
|
||||
return result['certificate']
|
||||
|
||||
def _der_to_pem(self, der_cert):
|
||||
'''
|
||||
Convert the DER format certificate in der_cert to a PEM format
|
||||
certificate and return it.
|
||||
'''
|
||||
return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
|
||||
"\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64)))
|
||||
|
||||
def _download_cert(self, url):
|
||||
'''
|
||||
Download and parse the certificate chain.
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4.2
|
||||
'''
|
||||
content, info = self.account.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'})
|
||||
|
||||
if not content or not info['content-type'].startswith('application/pem-certificate-chain'):
|
||||
raise ModuleFailException("Cannot download certificate chain from {0}: {1} (headers: {2})".format(url, content, info))
|
||||
|
||||
cert = None
|
||||
chain = []
|
||||
|
||||
# Parse data
|
||||
lines = content.decode('utf-8').splitlines(True)
|
||||
current = []
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
current.append(line)
|
||||
if line.startswith('-----END CERTIFICATE-----'):
|
||||
if cert is None:
|
||||
cert = ''.join(current)
|
||||
else:
|
||||
chain.append(''.join(current))
|
||||
current = []
|
||||
|
||||
alternates = []
|
||||
|
||||
def f(link, relation):
|
||||
if relation == 'up':
|
||||
# Process link-up headers if there was no chain in reply
|
||||
if not chain:
|
||||
chain_result, chain_info = self.account.get_request(link, parse_json_result=False)
|
||||
if chain_info['status'] in [200, 201]:
|
||||
chain.append(self._der_to_pem(chain_result))
|
||||
elif relation == 'alternate':
|
||||
alternates.append(link)
|
||||
|
||||
process_links(info, f)
|
||||
|
||||
if cert is None or current:
|
||||
raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info))
|
||||
return {'cert': cert, 'chain': chain, 'alternates': alternates}
|
||||
|
||||
def _new_cert_v1(self):
|
||||
'''
|
||||
Create a new certificate based on the CSR (ACME v1 protocol).
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
|
||||
'''
|
||||
csr = pem_to_der(self.csr)
|
||||
new_cert = {
|
||||
"resource": "new-cert",
|
||||
"csr": nopad_b64(csr),
|
||||
}
|
||||
result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert)
|
||||
|
||||
chain = []
|
||||
|
||||
def f(link, relation):
|
||||
if relation == 'up':
|
||||
chain_result, chain_info = self.account.get_request(link, parse_json_result=False)
|
||||
if chain_info['status'] in [200, 201]:
|
||||
del chain[:]
|
||||
chain.append(self._der_to_pem(chain_result))
|
||||
|
||||
process_links(info, f)
|
||||
|
||||
if info['status'] not in [200, 201]:
|
||||
raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
else:
|
||||
return {'cert': self._der_to_pem(result), 'uri': info['location'], 'chain': chain}
|
||||
|
||||
def _new_order_v2(self):
|
||||
'''
|
||||
Start a new certificate order (ACME v2 protocol).
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
'''
|
||||
identifiers = []
|
||||
for identifier_type, identifier in self.identifiers:
|
||||
identifiers.append({
|
||||
'type': identifier_type,
|
||||
'value': identifier,
|
||||
})
|
||||
new_order = {
|
||||
"identifiers": identifiers
|
||||
}
|
||||
result, info = self.account.send_signed_request(self.directory['newOrder'], new_order)
|
||||
|
||||
if info['status'] not in [201]:
|
||||
raise ModuleFailException("Error new order: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
|
||||
for auth_uri in result['authorizations']:
|
||||
auth_data, dummy = self.account.get_request(auth_uri)
|
||||
auth_data['uri'] = auth_uri
|
||||
identifier_type = auth_data['identifier']['type']
|
||||
identifier = auth_data['identifier']['value']
|
||||
if auth_data.get('wildcard', False):
|
||||
identifier = '*.{0}'.format(identifier)
|
||||
self.authorizations[identifier_type + ':' + identifier] = auth_data
|
||||
|
||||
self.order_uri = info['location']
|
||||
self.finalize_uri = result['finalize']
|
||||
self.identifiers = self.client.backend.get_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
|
||||
|
||||
def is_first_step(self):
|
||||
'''
|
||||
@@ -922,28 +655,33 @@ class ACMEClient(object):
|
||||
if identifier_type != 'dns':
|
||||
raise ModuleFailException('ACME v1 only supports DNS identifiers!')
|
||||
for identifier_type, identifier in self.identifiers:
|
||||
new_auth = self._new_authz_v1(identifier_type, identifier)
|
||||
self._add_or_update_auth(identifier_type, identifier, new_auth)
|
||||
authz = Authorization.create(self.client, identifier_type, identifier)
|
||||
self.authorizations[authz.combined_identifier] = authz
|
||||
else:
|
||||
self._new_order_v2()
|
||||
self.order = Order.create(self.client, self.identifiers)
|
||||
self.order_uri = self.order.url
|
||||
self.order.load_authorizations(self.client)
|
||||
self.authorizations.update(self.order.authorizations)
|
||||
self.changed = True
|
||||
|
||||
def get_challenges_data(self):
|
||||
def get_challenges_data(self, first_step):
|
||||
'''
|
||||
Get challenge details for the chosen challenge type.
|
||||
Return a tuple of generic challenge details, and specialized DNS challenge details.
|
||||
'''
|
||||
# Get general challenge data
|
||||
data = {}
|
||||
for type_identifier, auth in self.authorizations.items():
|
||||
identifier_type, identifier = type_identifier.split(':', 1)
|
||||
auth = self.authorizations[type_identifier]
|
||||
for type_identifier, authz in self.authorizations.items():
|
||||
identifier_type, identifier = split_identifier(type_identifier)
|
||||
# Skip valid authentications: their challenges are already valid
|
||||
# and do not need to be returned
|
||||
if auth['status'] == 'valid':
|
||||
if authz.status == 'valid':
|
||||
continue
|
||||
# We drop the type from the key to preserve backwards compatibility
|
||||
data[identifier] = self._get_challenge_data(auth, identifier_type, identifier)
|
||||
data[identifier] = authz.get_challenge_data(self.client)
|
||||
if first_step and self.challenge not in data[identifier]:
|
||||
raise ModuleFailException("Found no challenge of type '{0}' for identifier {1}!".format(
|
||||
self.challenge, type_identifier))
|
||||
# Get DNS challenge data
|
||||
data_dns = {}
|
||||
if self.challenge == 'dns-01':
|
||||
@@ -967,89 +705,40 @@ class ACMEClient(object):
|
||||
# For ACME v1, we attempt to create new authzs. Existing ones
|
||||
# will be returned instead.
|
||||
for identifier_type, identifier in self.identifiers:
|
||||
new_auth = self._new_authz_v1(identifier_type, identifier)
|
||||
self._add_or_update_auth(identifier_type, identifier, new_auth)
|
||||
authz = Authorization.create(self.client, identifier_type, identifier)
|
||||
self.authorizations[combine_identifier(identifier_type, identifier)] = authz
|
||||
else:
|
||||
# For ACME v2, we obtain the order object by fetching the
|
||||
# order URI, and extract the information from there.
|
||||
result, info = self.account.get_request(self.order_uri)
|
||||
self.order = Order.from_url(self.client, self.order_uri)
|
||||
self.order.load_authorizations(self.client)
|
||||
self.authorizations.update(self.order.authorizations)
|
||||
|
||||
if not result:
|
||||
raise ModuleFailException("Cannot download order from {0}: {1} (headers: {2})".format(self.order_uri, result, info))
|
||||
# Step 2: validate pending challenges
|
||||
for type_identifier, authz in self.authorizations.items():
|
||||
if authz.status == 'pending':
|
||||
identifier_type, identifier = split_identifier(type_identifier)
|
||||
authz.call_validate(self.client, self.challenge)
|
||||
self.changed = True
|
||||
|
||||
if info['status'] not in [200]:
|
||||
raise ModuleFailException("Error on downloading order: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
|
||||
for auth_uri in result['authorizations']:
|
||||
auth_data, dummy = self.account.get_request(auth_uri)
|
||||
auth_data['uri'] = auth_uri
|
||||
identifier_type = auth_data['identifier']['type']
|
||||
identifier = auth_data['identifier']['value']
|
||||
if auth_data.get('wildcard', False):
|
||||
identifier = '*.{0}'.format(identifier)
|
||||
self.authorizations[identifier_type + ':' + identifier] = auth_data
|
||||
|
||||
self.finalize_uri = result['finalize']
|
||||
|
||||
# Step 2: validate challenges
|
||||
for type_identifier, auth in self.authorizations.items():
|
||||
if auth['status'] == 'pending':
|
||||
identifier_type, identifier = type_identifier.split(':', 1)
|
||||
self._validate_challenges(identifier_type, identifier, auth)
|
||||
|
||||
def _chain_matches(self, chain, criterium):
|
||||
'''
|
||||
Check whether an alternate chain matches the specified criterium.
|
||||
'''
|
||||
if criterium['test_certificates'] == 'last':
|
||||
chain = chain[-1:]
|
||||
for cert in chain:
|
||||
def download_alternate_chains(self, cert):
|
||||
alternate_chains = []
|
||||
for alternate in cert.alternates:
|
||||
try:
|
||||
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend())
|
||||
matches = True
|
||||
if criterium['subject']:
|
||||
for k, v in parse_name_field(criterium['subject']):
|
||||
oid = cryptography_name_to_oid(k)
|
||||
value = to_native(v)
|
||||
found = False
|
||||
for attribute in x509.subject:
|
||||
if attribute.oid == oid and value == to_native(attribute.value):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
matches = False
|
||||
break
|
||||
if criterium['issuer']:
|
||||
for k, v in parse_name_field(criterium['issuer']):
|
||||
oid = cryptography_name_to_oid(k)
|
||||
value = to_native(v)
|
||||
found = False
|
||||
for attribute in x509.issuer:
|
||||
if attribute.oid == oid and value == to_native(attribute.value):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
matches = False
|
||||
break
|
||||
if criterium['subject_key_identifier']:
|
||||
try:
|
||||
ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier)
|
||||
if criterium['subject_key_identifier'] != ext.value.digest:
|
||||
matches = False
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
matches = False
|
||||
if criterium['authority_key_identifier']:
|
||||
try:
|
||||
ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier)
|
||||
if criterium['authority_key_identifier'] != ext.value.key_identifier:
|
||||
matches = False
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
matches = False
|
||||
if matches:
|
||||
return True
|
||||
except Exception as e:
|
||||
self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e))
|
||||
return False
|
||||
alt_cert = CertificateChain.download(self.client, alternate)
|
||||
except ModuleFailException as e:
|
||||
self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e))
|
||||
continue
|
||||
alternate_chains.append(alt_cert)
|
||||
return alternate_chains
|
||||
|
||||
def find_matching_chain(self, chains):
|
||||
for criterium_idx, matcher in enumerate(self.select_chain_matcher):
|
||||
for chain in chains:
|
||||
if matcher.match(chain):
|
||||
self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx))
|
||||
return chain
|
||||
return None
|
||||
|
||||
def get_certificate(self):
|
||||
'''
|
||||
@@ -1058,81 +747,46 @@ class ACMEClient(object):
|
||||
with an error.
|
||||
'''
|
||||
for identifier_type, identifier in self.identifiers:
|
||||
auth = self.authorizations.get(identifier_type + ':' + identifier)
|
||||
if auth is None:
|
||||
raise ModuleFailException('Found no authorization information for "{0}"!'.format(identifier_type + ':' + identifier))
|
||||
if 'status' not in auth:
|
||||
self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned no status')
|
||||
if auth['status'] != 'valid':
|
||||
self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned status ' + str(auth['status']))
|
||||
authz = self.authorizations.get(combine_identifier(identifier_type, identifier))
|
||||
if authz is None:
|
||||
raise ModuleFailException('Found no authorization information for "{identifier}"!'.format(
|
||||
identifier=combine_identifier(identifier_type, identifier)))
|
||||
if authz.status != 'valid':
|
||||
authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module)
|
||||
|
||||
if self.version == 1:
|
||||
cert = self._new_cert_v1()
|
||||
cert = retrieve_acme_v1_certificate(self.client, pem_to_der(self.csr, self.csr_content))
|
||||
else:
|
||||
cert_uri = self._finalize_cert()
|
||||
cert = self._download_cert(cert_uri)
|
||||
if self.module.params['retrieve_all_alternates'] or self.module.params['select_chain']:
|
||||
self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
|
||||
cert = CertificateChain.download(self.client, self.order.certificate_uri)
|
||||
if self.module.params['retrieve_all_alternates'] or self.select_chain_matcher:
|
||||
# Retrieve alternate chains
|
||||
alternate_chains = []
|
||||
for alternate in cert['alternates']:
|
||||
try:
|
||||
alt_cert = self._download_cert(alternate)
|
||||
except ModuleFailException as e:
|
||||
self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e))
|
||||
continue
|
||||
alternate_chains.append(alt_cert)
|
||||
alternate_chains = self.download_alternate_chains(cert)
|
||||
|
||||
# Prepare return value for all alternate chains
|
||||
if self.module.params['retrieve_all_alternates']:
|
||||
self.all_chains = []
|
||||
|
||||
def _append_all_chains(cert_data):
|
||||
self.all_chains.append(dict(
|
||||
cert=cert_data['cert'].encode('utf8'),
|
||||
chain=("\n".join(cert_data.get('chain', []))).encode('utf8'),
|
||||
full_chain=(cert_data['cert'] + "\n".join(cert_data.get('chain', []))).encode('utf8'),
|
||||
))
|
||||
|
||||
_append_all_chains(cert)
|
||||
self.all_chains = [cert.to_json()]
|
||||
for alt_chain in alternate_chains:
|
||||
_append_all_chains(alt_chain)
|
||||
self.all_chains.append(alt_chain.to_json())
|
||||
|
||||
# Try to select alternate chain depending on criteria
|
||||
if self.module.params['select_chain']:
|
||||
matching_chain = None
|
||||
all_chains = [cert] + alternate_chains
|
||||
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
|
||||
for v in ('subject_key_identifier', 'authority_key_identifier'):
|
||||
if criterium[v]:
|
||||
try:
|
||||
criterium[v] = binascii.unhexlify(criterium[v].replace(':', ''))
|
||||
except Exception:
|
||||
self.module.warn('Criterium {0} in select_chain has invalid {1} value. '
|
||||
'Ignoring criterium.'.format(criterium_idx, v))
|
||||
continue
|
||||
for alt_chain in all_chains:
|
||||
if self._chain_matches(alt_chain.get('chain', []), criterium):
|
||||
self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx))
|
||||
matching_chain = alt_chain
|
||||
break
|
||||
if matching_chain:
|
||||
break
|
||||
if self.select_chain_matcher:
|
||||
matching_chain = self.find_matching_chain([cert] + alternate_chains)
|
||||
if matching_chain:
|
||||
cert.update(matching_chain)
|
||||
cert = matching_chain
|
||||
else:
|
||||
self.module.debug('Found no matching alternative chain')
|
||||
|
||||
if cert['cert'] is not None:
|
||||
pem_cert = cert['cert']
|
||||
|
||||
chain = [link for link in cert.get('chain', [])]
|
||||
if cert.cert is not None:
|
||||
pem_cert = cert.cert
|
||||
chain = cert.chain
|
||||
|
||||
if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')):
|
||||
self.cert_days = get_cert_days(self.module, self.dest)
|
||||
self.cert_days = self.client.backend.get_cert_days(self.dest)
|
||||
self.changed = True
|
||||
|
||||
if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')):
|
||||
self.cert_days = get_cert_days(self.module, self.fullchain_dest)
|
||||
self.cert_days = self.client.backend.get_cert_days(self.fullchain_dest)
|
||||
self.changed = True
|
||||
|
||||
if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')):
|
||||
@@ -1144,25 +798,14 @@ class ACMEClient(object):
|
||||
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||
'''
|
||||
authz_deactivate = {
|
||||
'status': 'deactivated'
|
||||
}
|
||||
if self.version == 1:
|
||||
authz_deactivate['resource'] = 'authz'
|
||||
if self.authorizations:
|
||||
for identifier_type, identifier in self.identifiers:
|
||||
auth = self.authorizations.get(identifier_type + ':' + identifier)
|
||||
if auth is None or auth.get('status') != 'valid':
|
||||
continue
|
||||
try:
|
||||
result, info = self.account.send_signed_request(auth['uri'], authz_deactivate)
|
||||
if 200 <= info['status'] < 300 and result.get('status') == 'deactivated':
|
||||
auth['status'] = 'deactivated'
|
||||
except Exception as dummy:
|
||||
# Ignore errors on deactivating authzs
|
||||
pass
|
||||
if auth.get('status') != 'deactivated':
|
||||
self.module.warn(warning='Could not deactivate authz object {0}.'.format(auth['uri']))
|
||||
for authz in self.authorizations.values():
|
||||
try:
|
||||
authz.deactivate(self.client)
|
||||
except Exception:
|
||||
# ignore errors
|
||||
pass
|
||||
if authz.status != 'deactivated':
|
||||
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url))
|
||||
|
||||
|
||||
def main():
|
||||
@@ -1173,7 +816,8 @@ def main():
|
||||
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']),
|
||||
csr=dict(type='path', required=True, aliases=['src']),
|
||||
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']),
|
||||
@@ -1183,7 +827,7 @@ def main():
|
||||
force=dict(type='bool', default=False),
|
||||
retrieve_all_alternates=dict(type='bool', default=False),
|
||||
select_chain=dict(type='list', elements='dict', options=dict(
|
||||
test_certificates=dict(type='str', default='all', choices=['last', 'all']),
|
||||
test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']),
|
||||
issuer=dict(type='dict'),
|
||||
subject=dict(type='dict'),
|
||||
subject_key_identifier=dict(type='str'),
|
||||
@@ -1195,24 +839,21 @@ def main():
|
||||
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,
|
||||
)
|
||||
backend = handle_standard_module_arguments(module)
|
||||
if module.params['select_chain']:
|
||||
if backend != 'cryptography':
|
||||
module.fail_json(msg="The 'select_chain' can only be used with the 'cryptography' backend.")
|
||||
elif not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography'))
|
||||
backend = create_backend(module, False)
|
||||
|
||||
try:
|
||||
if module.params.get('dest'):
|
||||
cert_days = get_cert_days(module, module.params['dest'])
|
||||
cert_days = backend.get_cert_days(module.params['dest'])
|
||||
else:
|
||||
cert_days = get_cert_days(module, module.params['fullchain_dest'])
|
||||
cert_days = backend.get_cert_days(module.params['fullchain_dest'])
|
||||
|
||||
if module.params['force'] or cert_days < module.params['remaining_days']:
|
||||
# If checkmode is active, base the changed state solely on the status
|
||||
@@ -1222,10 +863,11 @@ def main():
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days)
|
||||
else:
|
||||
client = ACMEClient(module)
|
||||
client = ACMECertificateClient(module, backend)
|
||||
client.cert_days = cert_days
|
||||
other = dict()
|
||||
if client.is_first_step():
|
||||
is_first_step = client.is_first_step()
|
||||
if is_first_step:
|
||||
# First run: start challenges / start new order
|
||||
client.start_challenges()
|
||||
else:
|
||||
@@ -1233,22 +875,22 @@ def main():
|
||||
try:
|
||||
client.finish_challenges()
|
||||
client.get_certificate()
|
||||
if module.params['retrieve_all_alternates']:
|
||||
if client.all_chains is not None:
|
||||
other['all_chains'] = client.all_chains
|
||||
finally:
|
||||
if module.params['deactivate_authzs']:
|
||||
client.deactivate_authzs()
|
||||
data, data_dns = client.get_challenges_data()
|
||||
data, data_dns = client.get_challenges_data(first_step=is_first_step)
|
||||
auths = dict()
|
||||
for k, v in client.authorizations.items():
|
||||
# Remove "type:" from key
|
||||
auths[k.split(':', 1)[1]] = v
|
||||
auths[split_identifier(k)[1]] = v.to_json()
|
||||
module.exit_json(
|
||||
changed=client.changed,
|
||||
authorizations=auths,
|
||||
finalize_uri=client.finalize_uri,
|
||||
finalize_uri=client.order.finalize_uri if client.order else None,
|
||||
order_uri=client.order_uri,
|
||||
account_uri=client.account.uri,
|
||||
account_uri=client.client.account_uri,
|
||||
challenge_data=data,
|
||||
challenge_data_dns=data_dns,
|
||||
cert_days=client.cert_days,
|
||||
|
||||
@@ -25,6 +25,7 @@ notes:
|
||||
was different than the one specified here. Also, depending on the
|
||||
server, it can happen that some other error is returned if the
|
||||
certificate has already been revoked."
|
||||
- Does not support C(check_mode).
|
||||
seealso:
|
||||
- name: The Let's Encrypt documentation
|
||||
description: Documentation for the Let's Encrypt Certification Authority.
|
||||
@@ -53,7 +54,6 @@ options:
|
||||
private keys in PEM format can be used as well."
|
||||
- "Mutually exclusive with C(account_key_content)."
|
||||
- "Required if C(account_key_content) is not used."
|
||||
type: path
|
||||
account_key_content:
|
||||
description:
|
||||
- "Content of the ACME account RSA or Elliptic Curve key."
|
||||
@@ -68,7 +68,6 @@ options:
|
||||
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
|
||||
private_key_src:
|
||||
description:
|
||||
- "Path to the certificate's private key."
|
||||
@@ -90,6 +89,12 @@ options:
|
||||
Ansible in the process of moving the module with its argument to
|
||||
the node where it is executed."
|
||||
type: str
|
||||
private_key_passphrase:
|
||||
description:
|
||||
- Phassphrase to use to decode the certificate's private 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
|
||||
revoke_reason:
|
||||
description:
|
||||
- "One of the revocation reasonCodes defined in
|
||||
@@ -98,7 +103,7 @@ options:
|
||||
C(2) (cACompromise), C(3) (affiliationChanged), C(4) (superseded),
|
||||
C(5) (cessationOfOperation), C(6) (certificateHold),
|
||||
C(8) (removeFromCRL), C(9) (privilegeWithdrawn),
|
||||
C(10) (aACompromise)"
|
||||
C(10) (aACompromise)."
|
||||
type: int
|
||||
'''
|
||||
|
||||
@@ -114,18 +119,29 @@ EXAMPLES = '''
|
||||
certificate: /etc/httpd/ssl/sample.com.crt
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
RETURN = '''#'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
create_backend,
|
||||
get_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 (
|
||||
ACMEProtocolException,
|
||||
ModuleFailException,
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
pem_to_der,
|
||||
handle_standard_module_arguments,
|
||||
get_default_argspec,
|
||||
)
|
||||
|
||||
|
||||
@@ -134,6 +150,7 @@ def main():
|
||||
argument_spec.update(dict(
|
||||
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'),
|
||||
))
|
||||
@@ -147,10 +164,11 @@ def main():
|
||||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
handle_standard_module_arguments(module)
|
||||
backend = create_backend(module, False)
|
||||
|
||||
try:
|
||||
account = ACMEAccount(module)
|
||||
client = ACMEClient(module, backend)
|
||||
account = ACMEAccount(client)
|
||||
# Load certificate
|
||||
certificate = pem_to_der(module.params.get('certificate'))
|
||||
certificate = nopad_b64(certificate)
|
||||
@@ -162,25 +180,28 @@ def main():
|
||||
payload['reason'] = module.params.get('revoke_reason')
|
||||
# Determine endpoint
|
||||
if module.params.get('acme_version') == 1:
|
||||
endpoint = account.directory['revoke-cert']
|
||||
endpoint = client.directory['revoke-cert']
|
||||
payload['resource'] = 'revoke-cert'
|
||||
else:
|
||||
endpoint = account.directory['revokeCert']
|
||||
endpoint = client.directory['revokeCert']
|
||||
# Get hold of private key (if available) and make sure it comes from disk
|
||||
private_key = module.params.get('private_key_src')
|
||||
private_key_content = module.params.get('private_key_content')
|
||||
# Revoke certificate
|
||||
if private_key or private_key_content:
|
||||
passphrase = module.params['private_key_passphrase']
|
||||
# Step 1: load and parse private key
|
||||
error, private_key_data = account.parse_key(private_key, private_key_content)
|
||||
if error:
|
||||
raise ModuleFailException("error while parsing private key: %s" % error)
|
||||
try:
|
||||
private_key_data = client.parse_key(private_key, private_key_content, passphrase=passphrase)
|
||||
except KeyParsingError as e:
|
||||
raise ModuleFailException("Error while parsing private key: {msg}".format(msg=e.msg))
|
||||
# Step 2: sign revokation request with private key
|
||||
jws_header = {
|
||||
"alg": private_key_data['alg'],
|
||||
"jwk": private_key_data['jwk'],
|
||||
}
|
||||
result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header)
|
||||
result, info = client.send_signed_request(
|
||||
endpoint, payload, key_data=private_key_data, jws_header=jws_header, fail_on_error=False)
|
||||
else:
|
||||
# Step 1: get hold of account URI
|
||||
created, account_data = account.setup_account(allow_creation=False)
|
||||
@@ -189,7 +210,7 @@ def main():
|
||||
if account_data is None:
|
||||
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
||||
# Step 2: sign revokation request with account key
|
||||
result, info = account.send_signed_request(endpoint, payload)
|
||||
result, info = client.send_signed_request(endpoint, payload, fail_on_error=False)
|
||||
if info['status'] != 200:
|
||||
already_revoked = False
|
||||
# Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6)
|
||||
@@ -208,7 +229,7 @@ def main():
|
||||
# but successfully terminate while indicating no change
|
||||
if already_revoked:
|
||||
module.exit_json(changed=False)
|
||||
raise ModuleFailException('Error revoking certificate: {0} {1}'.format(info['status'], result))
|
||||
raise ACMEProtocolException(module, 'Failed to revoke certificate', info=info, content_json=result)
|
||||
module.exit_json(changed=True)
|
||||
except ModuleFailException as e:
|
||||
e.do_fail(module)
|
||||
|
||||
@@ -52,6 +52,13 @@ options:
|
||||
- "Content of the private key to use for this challenge certificate."
|
||||
- "Mutually exclusive with C(private_key_src)."
|
||||
type: str
|
||||
private_key_passphrase:
|
||||
description:
|
||||
- Phassphrase to use to decode the private key.
|
||||
type: str
|
||||
version_added: 1.6.0
|
||||
notes:
|
||||
- Does not support C(check_mode).
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
@@ -136,10 +143,13 @@ import sys
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
|
||||
read_file,
|
||||
)
|
||||
|
||||
@@ -156,7 +166,6 @@ try:
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
import ipaddress
|
||||
from distutils.version import LooseVersion
|
||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except ImportError as dummy:
|
||||
@@ -184,6 +193,7 @@ def main():
|
||||
challenge_data=dict(type='dict', required=True),
|
||||
private_key_src=dict(type='path'),
|
||||
private_key_content=dict(type='str', no_log=True),
|
||||
private_key_passphrase=dict(type='str', no_log=True),
|
||||
),
|
||||
required_one_of=(
|
||||
['private_key_src', 'private_key_content'],
|
||||
@@ -193,7 +203,10 @@ def main():
|
||||
),
|
||||
)
|
||||
if not HAS_CRYPTOGRAPHY:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
# Some callbacks die when exception is provided with value None
|
||||
if CRYPTOGRAPHY_IMP_ERR:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'))
|
||||
|
||||
try:
|
||||
# Get parameters
|
||||
@@ -202,12 +215,16 @@ def main():
|
||||
|
||||
# Get hold of private key
|
||||
private_key_content = module.params.get('private_key_content')
|
||||
private_key_passphrase = module.params.get('private_key_passphrase')
|
||||
if private_key_content is None:
|
||||
private_key_content = read_file(module.params['private_key_src'])
|
||||
else:
|
||||
private_key_content = to_bytes(private_key_content)
|
||||
try:
|
||||
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(private_key_content, password=None, backend=_cryptography_backend)
|
||||
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
private_key_content,
|
||||
password=to_bytes(private_key_passphrase) if private_key_passphrase is not None else None,
|
||||
backend=_cryptography_backend)
|
||||
except Exception as e:
|
||||
raise ModuleFailException('Error while loading private key: {0}'.format(e))
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ directory:
|
||||
description: The ACME directory's content
|
||||
returned: always
|
||||
type: dict
|
||||
sample: |
|
||||
sample:
|
||||
{
|
||||
"a85k3x9f91A4": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
|
||||
"keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
|
||||
@@ -203,7 +203,7 @@ headers:
|
||||
description: The request's HTTP headers (with lowercase keys)
|
||||
returned: always
|
||||
type: dict
|
||||
sample: |
|
||||
sample:
|
||||
{
|
||||
"boulder-requester": "12345",
|
||||
"cache-control": "max-age=0, no-cache, no-store",
|
||||
@@ -214,7 +214,7 @@ headers:
|
||||
"cookies_string": "",
|
||||
"date": "Wed, 07 Nov 2018 12:34:56 GMT",
|
||||
"expires": "Wed, 07 Nov 2018 12:44:56 GMT",
|
||||
"link": "<https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel=\"terms-of-service\"",
|
||||
"link": '<https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel="terms-of-service"',
|
||||
"msg": "OK (904 bytes)",
|
||||
"pragma": "no-cache",
|
||||
"replay-nonce": "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH",
|
||||
@@ -240,16 +240,18 @@ output_json:
|
||||
- ...
|
||||
'''
|
||||
|
||||
import json
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
ACMEAccount,
|
||||
handle_standard_module_arguments,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
create_backend,
|
||||
get_default_argspec,
|
||||
ACMEClient,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
ACMEProtocolException,
|
||||
ModuleFailException,
|
||||
)
|
||||
|
||||
|
||||
@@ -273,25 +275,26 @@ def main():
|
||||
['method', 'post', ['account_key_src', 'account_key_content'], True],
|
||||
),
|
||||
)
|
||||
handle_standard_module_arguments(module)
|
||||
backend = create_backend(module, False)
|
||||
|
||||
result = dict()
|
||||
changed = False
|
||||
try:
|
||||
# Get hold of ACMEAccount object (includes directory)
|
||||
account = ACMEAccount(module)
|
||||
# Get hold of ACMEClient and ACMEAccount objects (includes directory)
|
||||
client = ACMEClient(module, backend)
|
||||
method = module.params['method']
|
||||
result['directory'] = account.directory.directory
|
||||
result['directory'] = client.directory.directory
|
||||
# Do we have to do more requests?
|
||||
if method != 'directory-only':
|
||||
url = module.params['url']
|
||||
fail_on_acme_error = module.params['fail_on_acme_error']
|
||||
# Do request
|
||||
if method == 'get':
|
||||
data, info = account.get_request(url, parse_json_result=False, fail_on_error=False)
|
||||
data, info = client.get_request(url, parse_json_result=False, fail_on_error=False)
|
||||
elif method == 'post':
|
||||
changed = True # only POSTs can change
|
||||
data, info = account.send_signed_request(url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False)
|
||||
data, info = client.send_signed_request(
|
||||
url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False, fail_on_error=False)
|
||||
# Update results
|
||||
result.update(dict(
|
||||
headers=info,
|
||||
@@ -299,12 +302,12 @@ def main():
|
||||
))
|
||||
# See if we can parse the result as JSON
|
||||
try:
|
||||
result['output_json'] = json.loads(data)
|
||||
result['output_json'] = module.from_json(to_text(data))
|
||||
except Exception as dummy:
|
||||
pass
|
||||
# Fail if error was returned
|
||||
if fail_on_acme_error and info['status'] >= 400:
|
||||
raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], data))
|
||||
raise ACMEProtocolException(module, info=info, content=data)
|
||||
# Done!
|
||||
module.exit_json(changed=changed, **result)
|
||||
except ModuleFailException as e:
|
||||
|
||||
@@ -122,11 +122,18 @@ import os
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.exceptions
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
@@ -136,7 +143,6 @@ try:
|
||||
import cryptography.hazmat.primitives.asymmetric.utils
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
from distutils.version import LooseVersion
|
||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except ImportError as dummy:
|
||||
@@ -185,6 +191,9 @@ def is_parent(module, cert, potential_parent):
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature as dummy:
|
||||
return False
|
||||
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
|
||||
module.warn('Unsupported algorithm "{0}"'.format(cert.cert.signature_hash_algorithm))
|
||||
return False
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Unknown error on signature validation: {0}'.format(e))
|
||||
|
||||
@@ -194,27 +203,17 @@ def parse_PEM_list(module, text, source, fail_on_error=True):
|
||||
Parse concatenated PEM certificates. Return list of ``Certificate`` objects.
|
||||
'''
|
||||
result = []
|
||||
lines = text.splitlines(True)
|
||||
current = None
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
if line.startswith('-----BEGIN '):
|
||||
current = [line]
|
||||
elif current is not None:
|
||||
current.append(line)
|
||||
if line.startswith('-----END '):
|
||||
cert_pem = ''.join(current)
|
||||
current = None
|
||||
# Try to load PEM certificate
|
||||
try:
|
||||
cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend)
|
||||
result.append(Certificate(cert_pem, cert))
|
||||
except Exception as e:
|
||||
msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e)
|
||||
if fail_on_error:
|
||||
module.fail_json(msg=msg)
|
||||
else:
|
||||
module.warn(msg)
|
||||
for cert_pem in split_pem_list(text):
|
||||
# Try to load PEM certificate
|
||||
try:
|
||||
cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend)
|
||||
result.append(Certificate(cert_pem, cert))
|
||||
except Exception as e:
|
||||
msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e)
|
||||
if fail_on_error:
|
||||
module.fail_json(msg=msg)
|
||||
else:
|
||||
module.warn(msg)
|
||||
return result
|
||||
|
||||
|
||||
@@ -242,13 +241,17 @@ class CertificateSet(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.certificates = set()
|
||||
self.certificate_by_issuer = dict()
|
||||
self.certificates_by_issuer = dict()
|
||||
self.certificate_by_cert = dict()
|
||||
|
||||
def _load_file(self, path):
|
||||
certs = load_PEM_list(self.module, path, fail_on_error=False)
|
||||
for cert in certs:
|
||||
self.certificates.add(cert)
|
||||
self.certificate_by_issuer[cert.cert.subject] = cert
|
||||
if cert.cert.subject not in self.certificates_by_issuer:
|
||||
self.certificates_by_issuer[cert.cert.subject] = []
|
||||
self.certificates_by_issuer[cert.cert.subject].append(cert)
|
||||
self.certificate_by_cert[cert.cert] = cert
|
||||
|
||||
def load(self, path):
|
||||
'''
|
||||
@@ -266,8 +269,8 @@ class CertificateSet(object):
|
||||
'''
|
||||
Search for the parent (issuer) of a certificate. Return ``None`` if none was found.
|
||||
'''
|
||||
potential_parent = self.certificate_by_issuer.get(cert.cert.issuer)
|
||||
if potential_parent is not None:
|
||||
potential_parents = self.certificates_by_issuer.get(cert.cert.issuer, [])
|
||||
for potential_parent in potential_parents:
|
||||
if is_parent(self.module, cert, potential_parent):
|
||||
return potential_parent
|
||||
return None
|
||||
@@ -280,6 +283,16 @@ def format_cert(cert):
|
||||
return str(cert.cert)
|
||||
|
||||
|
||||
def check_cycle(module, occured_certificates, next):
|
||||
'''
|
||||
Make sure that next is not in occured_certificates so far, and add it.
|
||||
'''
|
||||
next_cert = next.cert
|
||||
if next_cert in occured_certificates:
|
||||
module.fail_json(msg='Found cycle while building certificate chain')
|
||||
occured_certificates.add(next_cert)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
@@ -318,13 +331,19 @@ def main():
|
||||
# Try to complete chain
|
||||
current = chain[-1]
|
||||
completed = []
|
||||
occured_certificates = set([cert.cert for cert in chain])
|
||||
if current.cert in roots.certificate_by_cert:
|
||||
# Don't try to complete the chain when it's already ending with a root certificate
|
||||
current = None
|
||||
while current:
|
||||
root = roots.find_parent(current)
|
||||
if root:
|
||||
check_cycle(module, occured_certificates, root)
|
||||
completed.append(root)
|
||||
break
|
||||
intermediate = intermediates.find_parent(current)
|
||||
if intermediate:
|
||||
check_cycle(module, occured_certificates, intermediate)
|
||||
completed.append(intermediate)
|
||||
current = intermediate
|
||||
else:
|
||||
|
||||
@@ -519,10 +519,10 @@ import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
write_file,
|
||||
@@ -922,8 +922,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']))
|
||||
|
||||
@@ -202,7 +202,7 @@ import datetime
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
|
||||
ecs_client_argument_spec,
|
||||
|
||||
@@ -18,6 +18,7 @@ description:
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL
|
||||
backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
- Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7.
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
@@ -34,6 +35,12 @@ options:
|
||||
- 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.
|
||||
@@ -43,6 +50,14 @@ options:
|
||||
- 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 C(mysql) currently.
|
||||
type: str
|
||||
choices:
|
||||
- mysql
|
||||
version_added: 1.9.0
|
||||
timeout:
|
||||
description:
|
||||
- The timeout in seconds
|
||||
@@ -152,13 +167,14 @@ import base64
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from os.path import isfile
|
||||
from socket import setdefaulttimeout, socket
|
||||
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_OPTIONAL
|
||||
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._text import to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_oid_to_name,
|
||||
@@ -182,7 +198,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
@@ -202,6 +218,20 @@ else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
def send_starttls_packet(sock, server_type):
|
||||
if server_type == 'mysql':
|
||||
ssl_request_packet = (
|
||||
b'\x20\x00\x00\x01\x85\xae\x7f\x00' +
|
||||
b'\x00\x00\x00\x01\x21\x00\x00\x00' +
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00' +
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00' +
|
||||
b'\x00\x00\x00\x00'
|
||||
)
|
||||
|
||||
sock.recv(8192) # discard initial handshake from server for this naive implementation
|
||||
sock.send(ssl_request_packet)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
@@ -210,8 +240,10 @@ def main():
|
||||
port=dict(type='int', required=True),
|
||||
proxy_host=dict(type='str'),
|
||||
proxy_port=dict(type='int', default=8080),
|
||||
server_name=dict(type='str'),
|
||||
timeout=dict(type='int', default=10),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
starttls=dict(type='str', choices=['mysql']),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -221,6 +253,8 @@ def main():
|
||||
proxy_host = module.params.get('proxy_host')
|
||||
proxy_port = module.params.get('proxy_port')
|
||||
timeout = module.params.get('timeout')
|
||||
server_name = module.params.get('server_name')
|
||||
start_tls_server_type = module.params.get('starttls')
|
||||
|
||||
backend = module.params.get('select_crypto_backend')
|
||||
if backend == 'auto':
|
||||
@@ -263,37 +297,50 @@ def main():
|
||||
if not isfile(ca_cert):
|
||||
module.fail_json(msg="ca_cert file does not exist")
|
||||
|
||||
if proxy_host:
|
||||
if not HAS_CREATE_DEFAULT_CONTEXT:
|
||||
if not HAS_CREATE_DEFAULT_CONTEXT:
|
||||
# Python < 2.7.9
|
||||
if proxy_host:
|
||||
module.fail_json(msg='To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.',
|
||||
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
|
||||
|
||||
try:
|
||||
connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port)
|
||||
sock = socket()
|
||||
atexit.register(sock.close)
|
||||
sock.connect((proxy_host, proxy_port))
|
||||
sock.send(connect.encode())
|
||||
sock.recv(8192)
|
||||
|
||||
ctx = create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = CERT_NONE
|
||||
|
||||
if ca_cert:
|
||||
ctx.verify_mode = CERT_OPTIONAL
|
||||
ctx.load_verify_locations(cafile=ca_cert)
|
||||
|
||||
cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True)
|
||||
cert = DER_cert_to_PEM_cert(cert)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to get cert from port with error: {0}".format(e))
|
||||
|
||||
else:
|
||||
try:
|
||||
# Note: get_server_certificate does not support SNI!
|
||||
cert = get_server_certificate((host, port), ca_certs=ca_cert)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to get cert from port with error: {0}".format(e))
|
||||
module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
|
||||
else:
|
||||
# Python >= 2.7.9
|
||||
try:
|
||||
if proxy_host:
|
||||
connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port)
|
||||
sock = socket()
|
||||
atexit.register(sock.close)
|
||||
sock.connect((proxy_host, proxy_port))
|
||||
sock.send(connect.encode())
|
||||
sock.recv(8192)
|
||||
else:
|
||||
sock = create_connection((host, port))
|
||||
atexit.register(sock.close)
|
||||
|
||||
if ca_cert:
|
||||
ctx = create_default_context(cafile=ca_cert)
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = CERT_REQUIRED
|
||||
else:
|
||||
ctx = create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = CERT_NONE
|
||||
|
||||
if start_tls_server_type is not None:
|
||||
send_starttls_packet(sock, start_tls_server_type)
|
||||
|
||||
cert = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True)
|
||||
cert = DER_cert_to_PEM_cert(cert)
|
||||
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(
|
||||
proxy_host, proxy_port, host, port, e))
|
||||
else:
|
||||
module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
|
||||
|
||||
result['cert'] = cert
|
||||
|
||||
|
||||
@@ -81,9 +81,10 @@ options:
|
||||
Needs I(keyfile) or I(passphrase) option for authorization.
|
||||
LUKS container supports up to 8 keyslots. Parameter value
|
||||
is the path to the keyfile with the passphrase."
|
||||
- "NOTE that adding additional keys is *not idempotent*.
|
||||
A new keyslot will be used even if another keyslot already
|
||||
exists for this keyfile."
|
||||
- "NOTE that adding additional keys is idempotent only since
|
||||
community.crypto 1.4.0. For older versions, a new keyslot
|
||||
will be used even if another keyslot already exists for this
|
||||
keyfile."
|
||||
- "BEWARE that working with keyfiles in plaintext is dangerous.
|
||||
Make sure that they are protected."
|
||||
type: path
|
||||
@@ -93,9 +94,9 @@ options:
|
||||
Needs I(keyfile) or I(passphrase) option for authorization. LUKS
|
||||
container supports up to 8 keyslots. Parameter value is a string
|
||||
with the new passphrase."
|
||||
- "NOTE that adding additional passphrase is *not idempotent*. A
|
||||
new keyslot will be used even if another keyslot already exists
|
||||
for this passphrase."
|
||||
- "NOTE that adding additional passphrase is idempotent only since
|
||||
community.crypto 1.4.0. For older versions, a new keyslot will
|
||||
be used even if another keyslot already exists for this passphrase."
|
||||
type: str
|
||||
version_added: '1.0.0'
|
||||
remove_keyfile:
|
||||
@@ -103,7 +104,8 @@ options:
|
||||
- "Removes given key from the container on I(device). Does not
|
||||
remove the keyfile from filesystem.
|
||||
Parameter value is the path to the keyfile with the passphrase."
|
||||
- "NOTE that removing keys is *not idempotent*. Trying to remove
|
||||
- "NOTE that removing keys is idempotent only since
|
||||
community.crypto 1.4.0. For older versions, trying to remove
|
||||
a key which no longer exists results in an error."
|
||||
- "NOTE that to remove the last key from a LUKS container, the
|
||||
I(force_remove_last_key) option must be set to C(yes)."
|
||||
@@ -114,9 +116,9 @@ options:
|
||||
description:
|
||||
- "Removes given passphrase from the container on I(device).
|
||||
Parameter value is a string with the passphrase to remove."
|
||||
- "NOTE that removing passphrases is I(not
|
||||
idempotent). Trying to remove a passphrase which no longer
|
||||
exists results in an error."
|
||||
- "NOTE that removing passphrases is idempotent only since
|
||||
community.crypto 1.4.0. For older versions, trying to remove
|
||||
a passphrase which no longer exists results in an error."
|
||||
- "NOTE that to remove the last keyslot from a LUKS
|
||||
container, the I(force_remove_last_key) option must be set
|
||||
to C(yes)."
|
||||
@@ -152,6 +154,69 @@ options:
|
||||
type: str
|
||||
choices: [luks1, luks2]
|
||||
version_added: '1.0.0'
|
||||
cipher:
|
||||
description:
|
||||
- "This option allows the user to define the cipher specification
|
||||
string for the LUKS container."
|
||||
- "Will only be used on container creation."
|
||||
- "For pre-2.6.10 kernels, use C(aes-plain) as they don't understand
|
||||
the new cipher spec strings. To use ESSIV, use C(aes-cbc-essiv:sha256)."
|
||||
type: str
|
||||
version_added: '1.1.0'
|
||||
hash:
|
||||
description:
|
||||
- "This option allows the user to specify the hash function used in LUKS
|
||||
key setup scheme and volume key digest."
|
||||
- "Will only be used on container creation."
|
||||
type: str
|
||||
version_added: '1.1.0'
|
||||
pbkdf:
|
||||
description:
|
||||
- This option allows the user to configure the Password-Based Key Derivation
|
||||
Function (PBKDF) used.
|
||||
- Will only be used on container creation, and when adding keys to an existing
|
||||
container.
|
||||
type: dict
|
||||
version_added: '1.4.0'
|
||||
suboptions:
|
||||
iteration_time:
|
||||
description:
|
||||
- Specify the iteration time used for the PBKDF.
|
||||
- Note that this is in B(seconds), not in milliseconds as on the
|
||||
command line.
|
||||
- Mutually exclusive with I(iteration_count).
|
||||
type: float
|
||||
iteration_count:
|
||||
description:
|
||||
- Specify the iteration count used for the PBKDF.
|
||||
- Mutually exclusive with I(iteration_time).
|
||||
type: int
|
||||
algorithm:
|
||||
description:
|
||||
- The algorithm to use.
|
||||
- Only available for the LUKS 2 format.
|
||||
choices:
|
||||
- argon2i
|
||||
- argon2id
|
||||
- pbkdf2
|
||||
type: str
|
||||
memory:
|
||||
description:
|
||||
- The memory cost limit in kilobytes for the PBKDF.
|
||||
- This is not used for PBKDF2, but only for the Argon PBKDFs.
|
||||
type: int
|
||||
parallel:
|
||||
description:
|
||||
- The parallel cost for the PBKDF. This is the number of threads that
|
||||
run in parallel.
|
||||
- This is not used for PBKDF2, but only for the Argon PBKDFs.
|
||||
type: int
|
||||
sector_size:
|
||||
description:
|
||||
- "This option allows the user to specify the sector size (in bytes) used for LUKS2 containers."
|
||||
- "Will only be used on container creation."
|
||||
type: int
|
||||
version_added: '1.5.0'
|
||||
|
||||
requirements:
|
||||
- "cryptsetup"
|
||||
@@ -176,6 +241,13 @@ EXAMPLES = '''
|
||||
state: "present"
|
||||
passphrase: "foo"
|
||||
|
||||
- name: Create LUKS container with specific encryption
|
||||
community.crypto.luks_device:
|
||||
device: "/dev/loop0"
|
||||
state: "present"
|
||||
cipher: "aes"
|
||||
hash: "sha256"
|
||||
|
||||
- name: (Create and) open the LUKS container; name it "mycrypt"
|
||||
community.crypto.luks_device:
|
||||
device: "/dev/loop0"
|
||||
@@ -278,12 +350,40 @@ STDERR = 2
|
||||
|
||||
# used to get <luks-name> out of lsblk output in format 'crypt <luks-name>'
|
||||
# regex takes care of any possible blank characters
|
||||
LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
|
||||
LUKS_NAME_REGEX = re.compile(r'^crypt\s+([^\s]*)\s*$')
|
||||
# used to get </luks/device> out of lsblk output
|
||||
# in format 'device: </luks/device>'
|
||||
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
|
||||
|
||||
|
||||
# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf
|
||||
LUKS_HEADER = b'LUKS\xba\xbe'
|
||||
LUKS_HEADER_L = 6
|
||||
# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf
|
||||
LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000]
|
||||
LUKS2_HEADER2 = b'SKUL\xba\xbe'
|
||||
|
||||
|
||||
def wipe_luks_headers(device):
|
||||
wipe_offsets = []
|
||||
with open(device, 'rb') as f:
|
||||
# f.seek(0)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS_HEADER:
|
||||
wipe_offsets.append(0)
|
||||
for offset in LUKS2_HEADER_OFFSETS:
|
||||
f.seek(offset)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS2_HEADER2:
|
||||
wipe_offsets.append(offset)
|
||||
|
||||
if wipe_offsets:
|
||||
with open(device, 'wb') as f:
|
||||
for offset in wipe_offsets:
|
||||
f.seek(offset)
|
||||
f.write(b'\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
class Handler(object):
|
||||
|
||||
def __init__(self, module):
|
||||
@@ -346,13 +446,11 @@ class CryptHandler(Handler):
|
||||
raise ValueError('Error while obtaining LUKS name for %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
m = LUKS_NAME_REGEX.search(result[STDOUT])
|
||||
|
||||
try:
|
||||
name = m.group(1)
|
||||
except AttributeError:
|
||||
name = None
|
||||
return name
|
||||
for line in result[STDOUT].splitlines(False):
|
||||
m = LUKS_NAME_REGEX.match(line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
def get_container_device_by_name(self, name):
|
||||
''' obtain device name based on the LUKS container name
|
||||
@@ -374,7 +472,19 @@ class CryptHandler(Handler):
|
||||
result = self._run_command([self._cryptsetup_bin, 'isLuks', device])
|
||||
return result[RETURN_CODE] == 0
|
||||
|
||||
def run_luks_create(self, device, keyfile, passphrase, keysize):
|
||||
def _add_pbkdf_options(self, options, pbkdf):
|
||||
if pbkdf['iteration_time'] is not None:
|
||||
options.extend(['--iter-time', str(int(pbkdf['iteration_time'] * 1000))])
|
||||
if pbkdf['iteration_count'] is not None:
|
||||
options.extend(['--pbkdf-force-iterations', str(pbkdf['iteration_count'])])
|
||||
if pbkdf['algorithm'] is not None:
|
||||
options.extend(['--pbkdf', pbkdf['algorithm']])
|
||||
if pbkdf['memory'] is not None:
|
||||
options.extend(['--pbkdf-memory', str(pbkdf['memory'])])
|
||||
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):
|
||||
# create a new luks container; use batch mode to auto confirm
|
||||
luks_type = self._module.params['type']
|
||||
label = self._module.params['label']
|
||||
@@ -387,6 +497,14 @@ class CryptHandler(Handler):
|
||||
luks_type = 'luks2'
|
||||
if luks_type is not None:
|
||||
options.extend(['--type', luks_type])
|
||||
if cipher is not None:
|
||||
options.extend(['--cipher', cipher])
|
||||
if hash_ is not None:
|
||||
options.extend(['--hash', hash_])
|
||||
if pbkdf is not None:
|
||||
self._add_pbkdf_options(options, pbkdf)
|
||||
if sector_size is not None:
|
||||
options.extend(['--sector-size', str(sector_size)])
|
||||
|
||||
args = [self._cryptsetup_bin, 'luksFormat']
|
||||
args.extend(options)
|
||||
@@ -423,17 +541,27 @@ class CryptHandler(Handler):
|
||||
self.run_luks_close(name)
|
||||
result = self._run_command([wipefs_bin, '--all', device])
|
||||
if result[RETURN_CODE] != 0:
|
||||
raise ValueError('Error while wiping luks container %s: %s'
|
||||
raise ValueError('Error while wiping LUKS container signatures for %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
# For LUKS2, sometimes both `cryptsetup erase` and `wipefs` do **not**
|
||||
# erase all LUKS signatures (they seem to miss the second header). That's
|
||||
# why we do it ourselves here.
|
||||
try:
|
||||
wipe_luks_headers(device)
|
||||
except Exception as exc:
|
||||
raise ValueError('Error while wiping LUKS container signatures for %s: %s' % (device, exc))
|
||||
|
||||
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
|
||||
new_passphrase):
|
||||
new_passphrase, pbkdf):
|
||||
''' Add new key from a keyfile or passphrase to given 'device';
|
||||
authentication done using 'keyfile' or 'passphrase'.
|
||||
Raises ValueError when command fails.
|
||||
'''
|
||||
data = []
|
||||
args = [self._cryptsetup_bin, 'luksAddKey', device]
|
||||
if pbkdf is not None:
|
||||
self._add_pbkdf_options(args, pbkdf)
|
||||
|
||||
if keyfile:
|
||||
args.extend(['--key-file', keyfile])
|
||||
@@ -493,6 +621,28 @@ class CryptHandler(Handler):
|
||||
raise ValueError('Error while removing LUKS key from %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
def luks_test_key(self, device, keyfile, passphrase):
|
||||
''' Check whether the keyfile or passphrase works.
|
||||
Raises ValueError when command fails.
|
||||
'''
|
||||
data = None
|
||||
args = [self._cryptsetup_bin, 'luksOpen', '--test-passphrase', device]
|
||||
|
||||
if keyfile:
|
||||
args.extend(['--key-file', keyfile])
|
||||
else:
|
||||
data = passphrase
|
||||
|
||||
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
|
||||
|
||||
raise ValueError('Error while testing whether keyslot exists on %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
|
||||
class ConditionsHandler(Handler):
|
||||
|
||||
@@ -600,7 +750,7 @@ class ConditionsHandler(Handler):
|
||||
self._module.fail_json(msg="Contradiction in setup: Asking to "
|
||||
"add a key to absent LUKS.")
|
||||
|
||||
return True
|
||||
return not self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'])
|
||||
|
||||
def luks_remove_key(self):
|
||||
if (self.device is None or
|
||||
@@ -613,7 +763,7 @@ class ConditionsHandler(Handler):
|
||||
self._module.fail_json(msg="Contradiction in setup: Asking to "
|
||||
"remove a key from absent LUKS.")
|
||||
|
||||
return True
|
||||
return self._crypthandler.luks_test_key(self.device, self._module.params['remove_keyfile'], self._module.params['remove_passphrase'])
|
||||
|
||||
def luks_remove(self):
|
||||
return (self.device is not None and
|
||||
@@ -638,6 +788,20 @@ def run_module():
|
||||
label=dict(type='str'),
|
||||
uuid=dict(type='str'),
|
||||
type=dict(type='str', choices=['luks1', 'luks2']),
|
||||
cipher=dict(type='str'),
|
||||
hash=dict(type='str'),
|
||||
pbkdf=dict(
|
||||
type='dict',
|
||||
options=dict(
|
||||
iteration_time=dict(type='float'),
|
||||
iteration_count=dict(type='int'),
|
||||
algorithm=dict(type='str', choices=['argon2i', 'argon2id', 'pbkdf2']),
|
||||
memory=dict(type='int'),
|
||||
parallel=dict(type='int'),
|
||||
),
|
||||
mutually_exclusive=[('iteration_time', 'iteration_count')],
|
||||
),
|
||||
sector_size=dict(type='int'),
|
||||
)
|
||||
|
||||
mutually_exclusive = [
|
||||
@@ -655,6 +819,7 @@ def run_module():
|
||||
module = AnsibleModule(argument_spec=module_args,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=mutually_exclusive)
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
|
||||
if module.params['device'] is not None:
|
||||
try:
|
||||
@@ -682,7 +847,12 @@ def run_module():
|
||||
crypt.run_luks_create(conditions.device,
|
||||
module.params['keyfile'],
|
||||
module.params['passphrase'],
|
||||
module.params['keysize'])
|
||||
module.params['keysize'],
|
||||
module.params['cipher'],
|
||||
module.params['hash'],
|
||||
module.params['sector_size'],
|
||||
module.params['pbkdf'],
|
||||
)
|
||||
except ValueError as e:
|
||||
module.fail_json(msg="luks_device error: %s" % e)
|
||||
result['changed'] = True
|
||||
@@ -743,7 +913,8 @@ def run_module():
|
||||
module.params['keyfile'],
|
||||
module.params['passphrase'],
|
||||
module.params['new_keyfile'],
|
||||
module.params['new_passphrase'])
|
||||
module.params['new_passphrase'],
|
||||
module.params['pbkdf'])
|
||||
except ValueError as e:
|
||||
module.fail_json(msg="luks_device error: %s" % e)
|
||||
result['changed'] = True
|
||||
|
||||
@@ -20,7 +20,8 @@ requirements:
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Whether the host or user certificate should exist or not, taking action if the state is different from what is stated.
|
||||
- Whether the host or user certificate should exist or not, taking action if the state is different
|
||||
from what is stated.
|
||||
type: str
|
||||
default: "present"
|
||||
choices: [ 'present', 'absent' ]
|
||||
@@ -33,6 +34,7 @@ options:
|
||||
force:
|
||||
description:
|
||||
- Should the certificate be regenerated even if it already exists and is valid.
|
||||
- Equivalent to I(regenerate=always).
|
||||
type: bool
|
||||
default: false
|
||||
path:
|
||||
@@ -40,11 +42,65 @@ options:
|
||||
- Path of the file containing the certificate.
|
||||
type: path
|
||||
required: true
|
||||
regenerate:
|
||||
description:
|
||||
- When C(never) the task will fail if a certificate already exists at I(path) and is unreadable
|
||||
otherwise a new certificate will only be generated if there is no existing certificate.
|
||||
- When C(fail) the task will fail if a certificate already exists at I(path) and does not
|
||||
match the module's options.
|
||||
- When C(partial_idempotence) an existing certificate will be regenerated based on
|
||||
I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals).
|
||||
- When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key)
|
||||
are also considered when compared against an existing certificate.
|
||||
- C(always) is equivalent to I(force=true).
|
||||
type: str
|
||||
choices:
|
||||
- never
|
||||
- fail
|
||||
- partial_idempotence
|
||||
- full_idempotence
|
||||
- always
|
||||
default: partial_idempotence
|
||||
version_added: 1.8.0
|
||||
signature_algorithm:
|
||||
description:
|
||||
- As of OpenSSH 8.2 the SHA-1 signature algorithm for RSA keys has been disabled and C(ssh) will refuse
|
||||
host certificates signed with the SHA-1 algorithm. OpenSSH 8.1 made C(rsa-sha2-512) the default algorithm
|
||||
when acting as a CA and signing certificates with a RSA key. However, for OpenSSH versions less than 8.1
|
||||
the SHA-2 signature algorithms, C(rsa-sha2-256) or C(rsa-sha2-512), must be specified using this option
|
||||
if compatibility with newer C(ssh) clients is required. Conversely if hosts using OpenSSH version 8.2
|
||||
or greater must remain compatible with C(ssh) clients using OpenSSH less than 7.2, then C(ssh-rsa)
|
||||
can be used when generating host certificates (a corresponding change to the sshd_config to add C(ssh-rsa)
|
||||
to the C(CASignatureAlgorithms) keyword is also required).
|
||||
- Using any value for this option with a non-RSA I(signing_key) will cause this module to fail.
|
||||
- "Note: OpenSSH versions prior to 7.2 do not support SHA-2 signature algorithms for RSA keys and OpenSSH
|
||||
versions prior to 7.3 do not support SHA-2 signature algorithms for certificates."
|
||||
- See U(https://www.openssh.com/txt/release-8.2) for more information.
|
||||
type: str
|
||||
choices:
|
||||
- ssh-rsa
|
||||
- rsa-sha2-256
|
||||
- rsa-sha2-512
|
||||
version_added: 1.10.0
|
||||
signing_key:
|
||||
description:
|
||||
- The path to the private openssh key that is used for signing the public key in order to generate the certificate.
|
||||
- If the private key is on a PKCS#11 token (I(pkcs11_provider)), set this to the path to the public key instead.
|
||||
- Required if I(state) is C(present).
|
||||
type: path
|
||||
pkcs11_provider:
|
||||
description:
|
||||
- To use a signing key that resides on a PKCS#11 token, set this to the name (or full path) of the shared library to use with the token.
|
||||
Usually C(libpkcs11.so).
|
||||
- If this is set, I(signing_key) needs to point to a file containing the public key of the CA.
|
||||
type: str
|
||||
version_added: 1.1.0
|
||||
use_agent:
|
||||
description:
|
||||
- Should the ssh-keygen use a CA key residing in a ssh-agent.
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 1.3.0
|
||||
public_key:
|
||||
description:
|
||||
- The path to the public key that will be signed with the signing key in order to generate the certificate.
|
||||
@@ -86,7 +142,7 @@ options:
|
||||
command specified by the user when the certificate is used for authentication."
|
||||
- "C(no-agent-forwarding): Disable ssh-agent forwarding (permitted by default)."
|
||||
- "C(no-port-forwarding): Disable port forwarding (permitted by default)."
|
||||
- "C(no-pty Disable): PTY allocation (permitted by default)."
|
||||
- "C(no-pty): Disable PTY allocation (permitted by default)."
|
||||
- "C(no-user-rc): Disable execution of C(~/.ssh/rc) by sshd (permitted by default)."
|
||||
- "C(no-x11-forwarding): Disable X11 forwarding (permitted by default)"
|
||||
- "C(permit-agent-forwarding): Allows ssh-agent forwarding."
|
||||
@@ -170,6 +226,16 @@ EXAMPLES = '''
|
||||
- "clear"
|
||||
- "force-command=/tmp/bla/foo"
|
||||
|
||||
- name: Generate an OpenSSH user certificate using a PKCS#11 token
|
||||
community.crypto.openssh_cert:
|
||||
type: user
|
||||
signing_key: /path/to/ca_public_key.pub
|
||||
pkcs11_provider: libpkcs11.so
|
||||
public_key: /path/to/public_key.pub
|
||||
path: /path/to/certificate
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
@@ -191,394 +257,293 @@ info:
|
||||
|
||||
'''
|
||||
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import MINYEAR, MAXYEAR
|
||||
from shutil import copy2, rmtree
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
|
||||
KeygenCommand,
|
||||
OpensshModule,
|
||||
PrivateKey,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import (
|
||||
OpensshCertificate,
|
||||
OpensshCertificateTimeParameters,
|
||||
parse_option_list,
|
||||
)
|
||||
|
||||
|
||||
class CertificateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Certificate(object):
|
||||
|
||||
class Certificate(OpensshModule):
|
||||
def __init__(self, module):
|
||||
self.state = module.params['state']
|
||||
self.force = module.params['force']
|
||||
self.type = module.params['type']
|
||||
self.signing_key = module.params['signing_key']
|
||||
self.public_key = module.params['public_key']
|
||||
self.path = module.params['path']
|
||||
self.identifier = module.params['identifier']
|
||||
self.serial_number = module.params['serial_number']
|
||||
self.valid_from = module.params['valid_from']
|
||||
self.valid_to = module.params['valid_to']
|
||||
self.valid_at = module.params['valid_at']
|
||||
self.principals = module.params['principals']
|
||||
self.options = module.params['options']
|
||||
self.changed = False
|
||||
self.check_mode = module.check_mode
|
||||
self.cert_info = {}
|
||||
super(Certificate, self).__init__(module)
|
||||
self.ssh_keygen = KeygenCommand(self.module)
|
||||
|
||||
self.identifier = self.module.params['identifier'] or ""
|
||||
self.options = self.module.params['options'] or []
|
||||
self.path = self.module.params['path']
|
||||
self.pkcs11_provider = self.module.params['pkcs11_provider']
|
||||
self.principals = self.module.params['principals'] or []
|
||||
self.public_key = self.module.params['public_key']
|
||||
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||
self.serial_number = self.module.params['serial_number']
|
||||
self.signature_algorithm = self.module.params['signature_algorithm']
|
||||
self.signing_key = self.module.params['signing_key']
|
||||
self.state = self.module.params['state']
|
||||
self.type = self.module.params['type']
|
||||
self.use_agent = self.module.params['use_agent']
|
||||
self.valid_at = self.module.params['valid_at']
|
||||
|
||||
self._check_if_base_dir(self.path)
|
||||
|
||||
if self.state == 'present':
|
||||
self._validate_parameters()
|
||||
|
||||
if self.options and self.type == "host":
|
||||
module.fail_json(msg="Options can only be used with user certificates.")
|
||||
self.data = None
|
||||
self.original_data = None
|
||||
if self._exists():
|
||||
self._load_certificate()
|
||||
|
||||
if self.valid_at:
|
||||
self.valid_at = self.valid_at.lstrip()
|
||||
self.time_parameters = None
|
||||
if self.state == 'present':
|
||||
self._set_time_parameters()
|
||||
|
||||
self.valid_from = self.valid_from.lstrip()
|
||||
self.valid_to = self.valid_to.lstrip()
|
||||
def _validate_parameters(self):
|
||||
for path in (self.public_key, self.signing_key):
|
||||
self._check_if_base_dir(path)
|
||||
|
||||
self.ssh_keygen = module.get_bin_path('ssh-keygen', True)
|
||||
if self.options and self.type == "host":
|
||||
self.module.fail_json(msg="Options can only be used with user certificates.")
|
||||
|
||||
def generate(self, module):
|
||||
if self.use_agent:
|
||||
self._use_agent_available()
|
||||
|
||||
if not self.is_valid(module, perms_required=False) or self.force:
|
||||
args = [
|
||||
self.ssh_keygen,
|
||||
'-s', self.signing_key
|
||||
]
|
||||
def _use_agent_available(self):
|
||||
ssh_version = self._get_ssh_version()
|
||||
if not ssh_version:
|
||||
self.module.fail_json(msg="Failed to determine ssh version")
|
||||
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||
self.module.fail_json(
|
||||
msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." +
|
||||
" Your version is: %s" % ssh_version
|
||||
)
|
||||
|
||||
validity = ""
|
||||
def _exists(self):
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if not (self.valid_from == "always" and self.valid_to == "forever"):
|
||||
|
||||
if not self.valid_from == "always":
|
||||
timeobj = self.convert_to_datetime(module, self.valid_from)
|
||||
validity += (
|
||||
str(timeobj.year).zfill(4) +
|
||||
str(timeobj.month).zfill(2) +
|
||||
str(timeobj.day).zfill(2) +
|
||||
str(timeobj.hour).zfill(2) +
|
||||
str(timeobj.minute).zfill(2) +
|
||||
str(timeobj.second).zfill(2)
|
||||
)
|
||||
else:
|
||||
validity += "19700101010101"
|
||||
|
||||
validity += ":"
|
||||
|
||||
if self.valid_to == "forever":
|
||||
# on ssh-keygen versions that have the year 2038 bug this will cause the datetime to be 2038-01-19T04:14:07
|
||||
timeobj = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
timeobj = self.convert_to_datetime(module, self.valid_to)
|
||||
|
||||
validity += (
|
||||
str(timeobj.year).zfill(4) +
|
||||
str(timeobj.month).zfill(2) +
|
||||
str(timeobj.day).zfill(2) +
|
||||
str(timeobj.hour).zfill(2) +
|
||||
str(timeobj.minute).zfill(2) +
|
||||
str(timeobj.second).zfill(2)
|
||||
)
|
||||
|
||||
args.extend(["-V", validity])
|
||||
|
||||
if self.type == 'host':
|
||||
args.extend(['-h'])
|
||||
|
||||
if self.identifier:
|
||||
args.extend(['-I', self.identifier])
|
||||
else:
|
||||
args.extend(['-I', ""])
|
||||
|
||||
if self.serial_number is not None:
|
||||
args.extend(['-z', str(self.serial_number)])
|
||||
|
||||
if self.principals:
|
||||
args.extend(['-n', ','.join(self.principals)])
|
||||
|
||||
if self.options:
|
||||
for option in self.options:
|
||||
args.extend(['-O'])
|
||||
args.extend([option])
|
||||
|
||||
args.extend(['-P', ''])
|
||||
|
||||
try:
|
||||
temp_directory = tempfile.mkdtemp()
|
||||
copy2(self.public_key, temp_directory)
|
||||
args.extend([temp_directory + "/" + os.path.basename(self.public_key)])
|
||||
module.run_command(args, environ_update=dict(TZ="UTC"), check_rc=True)
|
||||
copy2(temp_directory + "/" + os.path.splitext(os.path.basename(self.public_key))[0] + "-cert.pub", self.path)
|
||||
rmtree(temp_directory, ignore_errors=True)
|
||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path])
|
||||
self.cert_info = proc[1].split()
|
||||
self.changed = True
|
||||
except Exception as e:
|
||||
try:
|
||||
self.remove()
|
||||
rmtree(temp_directory, ignore_errors=True)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise CertificateError(exc)
|
||||
else:
|
||||
pass
|
||||
module.fail_json(msg="%s" % to_native(e))
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
|
||||
def convert_to_datetime(self, module, timestring):
|
||||
|
||||
if self.is_relative(timestring):
|
||||
result = convert_relative_to_datetime(timestring)
|
||||
if result is None:
|
||||
module.fail_json(
|
||||
msg="'%s' is not a valid time format." % timestring)
|
||||
else:
|
||||
return result
|
||||
else:
|
||||
formats = ["%Y-%m-%d",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%dT%H:%M:%S",
|
||||
]
|
||||
for fmt in formats:
|
||||
try:
|
||||
return datetime.strptime(timestring, fmt)
|
||||
except ValueError:
|
||||
pass
|
||||
module.fail_json(msg="'%s' is not a valid time format" % timestring)
|
||||
|
||||
def is_relative(self, timestr):
|
||||
if timestr.startswith("+") or timestr.startswith("-"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_same_datetime(self, datetime_one, datetime_two):
|
||||
|
||||
# This function is for backwards compatibility only because .total_seconds() is new in python2.7
|
||||
def timedelta_total_seconds(time_delta):
|
||||
return (time_delta.microseconds + 0.0 + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
|
||||
# try to use .total_ seconds() from python2.7
|
||||
def _load_certificate(self):
|
||||
try:
|
||||
return (datetime_one - datetime_two).total_seconds() == 0.0
|
||||
except AttributeError:
|
||||
return timedelta_total_seconds(datetime_one - datetime_two) == 0.0
|
||||
self.original_data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
if self.regenerate in ('never', 'fail'):
|
||||
self.module.fail_json(msg="Unable to read existing certificate: %s" % to_native(e))
|
||||
self.module.warn("Unable to read existing certificate: %s" % to_native(e))
|
||||
|
||||
def is_valid(self, module, perms_required=True):
|
||||
|
||||
def _check_state():
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if _check_state():
|
||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path], environ_update=dict(TZ="UTC"), check_rc=False)
|
||||
if proc[0] != 0:
|
||||
return False
|
||||
self.cert_info = proc[1].split()
|
||||
principals = re.findall("(?<=Principals:)(.*)(?=Critical)", proc[1], re.S)[0].split()
|
||||
principals = list(map(str.strip, principals))
|
||||
if principals == ["(none)"]:
|
||||
principals = None
|
||||
cert_type = re.findall("( user | host )", proc[1])[0].strip()
|
||||
serial_number = re.search(r"Serial: (\d+)", proc[1]).group(1)
|
||||
validity = re.findall("(from (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}) to (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}))", proc[1])
|
||||
if validity:
|
||||
if validity[0][1]:
|
||||
cert_valid_from = self.convert_to_datetime(module, validity[0][1])
|
||||
if self.is_same_datetime(cert_valid_from, self.convert_to_datetime(module, "1970-01-01 01:01:01")):
|
||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
||||
else:
|
||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
||||
|
||||
if validity[0][3]:
|
||||
cert_valid_to = self.convert_to_datetime(module, validity[0][3])
|
||||
if self.is_same_datetime(cert_valid_to, self.convert_to_datetime(module, "2038-01-19 03:14:07")):
|
||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
def _check_serial_number():
|
||||
if self.serial_number is None:
|
||||
return True
|
||||
return self.serial_number == int(serial_number)
|
||||
|
||||
def _check_type():
|
||||
return self.type == cert_type
|
||||
|
||||
def _check_principals():
|
||||
if not principals or not self.principals:
|
||||
return self.principals == principals
|
||||
return set(self.principals) == set(principals)
|
||||
|
||||
def _check_validity(module):
|
||||
if self.valid_from == "always":
|
||||
earliest_time = datetime(MINYEAR, 1, 1)
|
||||
elif self.is_relative(self.valid_from):
|
||||
earliest_time = None
|
||||
else:
|
||||
earliest_time = self.convert_to_datetime(module, self.valid_from)
|
||||
|
||||
if self.valid_to == "forever":
|
||||
last_time = datetime(MAXYEAR, 12, 31)
|
||||
elif self.is_relative(self.valid_to):
|
||||
last_time = None
|
||||
else:
|
||||
last_time = self.convert_to_datetime(module, self.valid_to)
|
||||
|
||||
if earliest_time:
|
||||
if not self.is_same_datetime(earliest_time, cert_valid_from):
|
||||
return False
|
||||
if last_time:
|
||||
if not self.is_same_datetime(last_time, cert_valid_to):
|
||||
return False
|
||||
|
||||
if self.valid_at:
|
||||
if cert_valid_from <= self.convert_to_datetime(module, self.valid_at) <= cert_valid_to:
|
||||
return True
|
||||
|
||||
if earliest_time and last_time:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if perms_required and not _check_perms(module):
|
||||
return False
|
||||
|
||||
return _check_type() and _check_principals() and _check_validity(module) and _check_serial_number()
|
||||
|
||||
def dump(self):
|
||||
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
def filter_keywords(arr, keywords):
|
||||
concated = []
|
||||
string = ""
|
||||
for word in arr:
|
||||
if word in keywords:
|
||||
concated.append(string)
|
||||
string = word
|
||||
else:
|
||||
string += " " + word
|
||||
concated.append(string)
|
||||
# drop the certificate path
|
||||
concated.pop(0)
|
||||
return concated
|
||||
|
||||
def format_cert_info():
|
||||
return filter_keywords(self.cert_info, [
|
||||
"Type:",
|
||||
"Public",
|
||||
"Signing",
|
||||
"Key",
|
||||
"Serial:",
|
||||
"Valid:",
|
||||
"Principals:",
|
||||
"Critical",
|
||||
"Extensions:"])
|
||||
def _set_time_parameters(self):
|
||||
try:
|
||||
self.time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=self.module.params['valid_from'],
|
||||
valid_to=self.module.params['valid_to'],
|
||||
)
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
def _execute(self):
|
||||
if self.state == 'present':
|
||||
result = {
|
||||
'changed': self.changed,
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'info': format_cert_info(),
|
||||
}
|
||||
if self._should_generate():
|
||||
self._generate()
|
||||
self._update_permissions(self.path)
|
||||
else:
|
||||
result = {
|
||||
'changed': self.changed,
|
||||
}
|
||||
if self._exists():
|
||||
self._remove()
|
||||
|
||||
return result
|
||||
def _should_generate(self):
|
||||
if self.regenerate == 'never':
|
||||
return self.original_data is None
|
||||
elif self.regenerate == 'fail':
|
||||
if self.original_data and not self._is_fully_valid():
|
||||
self.module.fail_json(
|
||||
msg="Certificate does not match the provided options.",
|
||||
cert=get_cert_dict(self.original_data)
|
||||
)
|
||||
return self.original_data is None
|
||||
elif self.regenerate == 'partial_idempotence':
|
||||
return self.original_data is None or not self._is_partially_valid()
|
||||
elif self.regenerate == 'full_idempotence':
|
||||
return self.original_data is None or not self._is_fully_valid()
|
||||
else:
|
||||
return True
|
||||
|
||||
def remove(self):
|
||||
"""Remove the resource from the filesystem."""
|
||||
def _is_fully_valid(self):
|
||||
return self._is_partially_valid() and all([
|
||||
self._compare_options() if self.original_data.type == 'user' else True,
|
||||
self.original_data.key_id == self.identifier,
|
||||
self.original_data.public_key == self._get_key_fingerprint(self.public_key),
|
||||
self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
|
||||
])
|
||||
|
||||
def _is_partially_valid(self):
|
||||
return all([
|
||||
set(self.original_data.principals) == set(self.principals),
|
||||
self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True,
|
||||
self.original_data.serial == self.serial_number if self.serial_number is not None else True,
|
||||
self.original_data.type == self.type,
|
||||
self._compare_time_parameters(),
|
||||
])
|
||||
|
||||
def _compare_time_parameters(self):
|
||||
try:
|
||||
original_time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=self.original_data.valid_after,
|
||||
valid_to=self.original_data.valid_before
|
||||
)
|
||||
except ValueError as e:
|
||||
return self.module.fail_json(msg=to_native(e))
|
||||
|
||||
return all([
|
||||
original_time_parameters == self.time_parameters,
|
||||
original_time_parameters.within_range(self.valid_at)
|
||||
])
|
||||
|
||||
def _compare_options(self):
|
||||
try:
|
||||
critical_options, extensions = parse_option_list(self.options)
|
||||
except ValueError as e:
|
||||
return self.module.fail_json(msg=to_native(e))
|
||||
|
||||
return all([
|
||||
set(self.original_data.critical_options) == set(critical_options),
|
||||
set(self.original_data.extensions) == set(extensions)
|
||||
])
|
||||
|
||||
def _get_key_fingerprint(self, path):
|
||||
private_key_content = self.ssh_keygen.get_private_key(path, check_rc=True)[1]
|
||||
return PrivateKey.from_string(private_key_content).fingerprint
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _generate(self):
|
||||
try:
|
||||
temp_certificate = self._generate_temp_certificate()
|
||||
self._safe_secure_move([(temp_certificate, self.path)])
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e)))
|
||||
|
||||
try:
|
||||
self.data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e))
|
||||
|
||||
def _generate_temp_certificate(self):
|
||||
key_copy = os.path.join(self.module.tmpdir, os.path.basename(self.public_key))
|
||||
|
||||
try:
|
||||
self.module.preserved_copy(self.public_key, key_copy)
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to stage temporary key: %s" % to_native(e))
|
||||
self.module.add_cleanup_file(key_copy)
|
||||
|
||||
self.ssh_keygen.generate_certificate(
|
||||
key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number,
|
||||
self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent,
|
||||
environ_update=dict(TZ="UTC"), check_rc=True
|
||||
)
|
||||
|
||||
temp_cert = os.path.splitext(key_copy)[0] + '-cert.pub'
|
||||
self.module.add_cleanup_file(temp_cert)
|
||||
|
||||
return temp_cert
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _remove(self):
|
||||
try:
|
||||
os.remove(self.path)
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise CertificateError(exc)
|
||||
else:
|
||||
pass
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
|
||||
|
||||
@property
|
||||
def _result(self):
|
||||
if self.state != 'present':
|
||||
return {}
|
||||
|
||||
certificate_info = self.ssh_keygen.get_certificate_info(self.path)[1]
|
||||
|
||||
return {
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'info': format_cert_info(certificate_info),
|
||||
}
|
||||
|
||||
@property
|
||||
def diff(self):
|
||||
return {
|
||||
'before': get_cert_dict(self.original_data),
|
||||
'after': get_cert_dict(self.data)
|
||||
}
|
||||
|
||||
|
||||
def format_cert_info(cert_info):
|
||||
result = []
|
||||
string = ""
|
||||
|
||||
for word in cert_info.split():
|
||||
if word in ("Type:", "Public", "Signing", "Key", "Serial:", "Valid:", "Principals:", "Critical", "Extensions:"):
|
||||
result.append(string)
|
||||
string = word
|
||||
else:
|
||||
string += " " + word
|
||||
result.append(string)
|
||||
# Drop the certificate path
|
||||
result.pop(0)
|
||||
return result
|
||||
|
||||
|
||||
def get_cert_dict(data):
|
||||
if data is None:
|
||||
return {}
|
||||
|
||||
result = data.to_dict()
|
||||
result.pop('nonce')
|
||||
result['signature_algorithm'] = data.signature_type
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
force=dict(type='bool', default=False),
|
||||
type=dict(type='str', choices=['host', 'user']),
|
||||
signing_key=dict(type='path'),
|
||||
public_key=dict(type='path'),
|
||||
path=dict(type='path', required=True),
|
||||
identifier=dict(type='str'),
|
||||
options=dict(type='list', elements='str'),
|
||||
path=dict(type='path', required=True),
|
||||
pkcs11_provider=dict(type='str'),
|
||||
principals=dict(type='list', elements='str'),
|
||||
public_key=dict(type='path'),
|
||||
regenerate=dict(
|
||||
type='str',
|
||||
default='partial_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']),
|
||||
signing_key=dict(type='path'),
|
||||
serial_number=dict(type='int'),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
type=dict(type='str', choices=['host', 'user']),
|
||||
use_agent=dict(type='bool', default=False),
|
||||
valid_at=dict(type='str'),
|
||||
valid_from=dict(type='str'),
|
||||
valid_to=dict(type='str'),
|
||||
valid_at=dict(type='str'),
|
||||
principals=dict(type='list', elements='str'),
|
||||
options=dict(type='list', elements='str'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
add_file_common_args=True,
|
||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
||||
)
|
||||
|
||||
def isBaseDir(path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
if module.params['state'] == "present":
|
||||
isBaseDir(module.params['signing_key'])
|
||||
isBaseDir(module.params['public_key'])
|
||||
|
||||
isBaseDir(module.params['path'])
|
||||
|
||||
certificate = Certificate(module)
|
||||
|
||||
if certificate.state == 'present':
|
||||
|
||||
if module.check_mode:
|
||||
certificate.changed = module.params['force'] or not certificate.is_valid(module)
|
||||
else:
|
||||
try:
|
||||
certificate.generate(module)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
else:
|
||||
|
||||
if module.check_mode:
|
||||
certificate.changed = os.path.exists(module.params['path'])
|
||||
if certificate.changed:
|
||||
certificate.cert_info = {}
|
||||
else:
|
||||
try:
|
||||
certificate.remove()
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
result = certificate.dump()
|
||||
module.exit_json(**result)
|
||||
Certificate(module).execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -7,18 +7,19 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: openssh_keypair
|
||||
author: "David Kainz (@lolcube)"
|
||||
short_description: Generate OpenSSH private and public keys.
|
||||
short_description: Generate OpenSSH private and public keys
|
||||
description:
|
||||
- "This module allows one to (re)generate OpenSSH private and public keys. It uses
|
||||
ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
|
||||
or C(ecdsa) private keys."
|
||||
requirements:
|
||||
- "ssh-keygen"
|
||||
- ssh-keygen (if I(backend=openssh))
|
||||
- cryptography >= 2.6 (if I(backend=cryptography) and OpenSSH < 7.8 is installed)
|
||||
- cryptography >= 3.0 (if I(backend=cryptography) and OpenSSH >= 7.8 is installed)
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
@@ -55,11 +56,40 @@ options:
|
||||
description:
|
||||
- Provides a new comment to the public key.
|
||||
type: str
|
||||
passphrase:
|
||||
description:
|
||||
- Passphrase used to decrypt an existing private key or encrypt a newly generated private key.
|
||||
- Passphrases are not supported for I(type=rsa1).
|
||||
- Can only be used when I(backend=cryptography), or when I(backend=auto) and a required C(cryptography) version is installed.
|
||||
type: str
|
||||
version_added: 1.7.0
|
||||
private_key_format:
|
||||
description:
|
||||
- Used when a I(backend=cryptography) to select a format for the private key at the provided I(path).
|
||||
- The only valid option currently is C(auto) which will match the key format of the installed OpenSSH version.
|
||||
- For OpenSSH < 7.8 private keys will be in PKCS1 format except ed25519 keys which will be in OpenSSH format.
|
||||
- For OpenSSH >= 7.8 all private key types will be in the OpenSSH format.
|
||||
type: str
|
||||
default: auto
|
||||
choices:
|
||||
- auto
|
||||
version_added: 1.7.0
|
||||
backend:
|
||||
description:
|
||||
- Selects between the C(cryptography) library or the OpenSSH binary C(opensshbin).
|
||||
- C(auto) will default to C(opensshbin) unless the OpenSSH binary is not installed or when using I(passphrase).
|
||||
type: str
|
||||
default: auto
|
||||
choices:
|
||||
- auto
|
||||
- cryptography
|
||||
- opensshbin
|
||||
version_added: 1.7.0
|
||||
regenerate:
|
||||
description:
|
||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
||||
The module will always generate a new key if the destination file does not exist.
|
||||
- By default, the key will be regenerated when it doesn't match the module's options,
|
||||
- By default, the key will be regenerated when it does not match the module's options,
|
||||
except when the key cannot be read or the passphrase does not match. Please note that
|
||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
||||
is specified.
|
||||
@@ -91,6 +121,8 @@ options:
|
||||
notes:
|
||||
- In case the ssh key is broken or password protected, the module will fail.
|
||||
Set the I(force) option to C(yes) if you want to regenerate the keypair.
|
||||
- Supports C(check_mode).
|
||||
- In the case a custom C(mode), C(group), C(owner), or other file attribute is provided it will be applied to both key files.
|
||||
|
||||
extends_documentation_fragment: files
|
||||
'''
|
||||
@@ -100,6 +132,11 @@ EXAMPLES = '''
|
||||
community.crypto.openssh_keypair:
|
||||
path: /tmp/id_ssh_rsa
|
||||
|
||||
- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) and encrypted private key
|
||||
community.crypto.openssh_keypair:
|
||||
path: /tmp/id_ssh_rsa
|
||||
passphrase: super_secret_password
|
||||
|
||||
- name: Generate an OpenSSH rsa keypair with a different size (2048 bits)
|
||||
community.crypto.openssh_keypair:
|
||||
path: /tmp/id_ssh_rsa
|
||||
@@ -118,17 +155,17 @@ EXAMPLES = '''
|
||||
|
||||
RETURN = '''
|
||||
size:
|
||||
description: Size (in bits) of the SSH private key
|
||||
description: Size (in bits) of the SSH private key.
|
||||
returned: changed or success
|
||||
type: int
|
||||
sample: 4096
|
||||
type:
|
||||
description: Algorithm used to generate the SSH private key
|
||||
description: Algorithm used to generate the SSH private key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: rsa
|
||||
filename:
|
||||
description: Path to the generated SSH private key file
|
||||
description: Path to the generated SSH private key file.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /tmp/id_ssh_rsa
|
||||
@@ -138,292 +175,26 @@ fingerprint:
|
||||
type: str
|
||||
sample: SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM
|
||||
public_key:
|
||||
description: The public key of the generated SSH private key
|
||||
description: The public key of the generated SSH private key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key
|
||||
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw==
|
||||
comment:
|
||||
description: The comment of the generated key
|
||||
description: The comment of the generated key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: test@comment
|
||||
'''
|
||||
|
||||
import errno
|
||||
import os
|
||||
import stat
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class KeypairError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Keypair(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.path = module.params['path']
|
||||
self.state = module.params['state']
|
||||
self.force = module.params['force']
|
||||
self.size = module.params['size']
|
||||
self.type = module.params['type']
|
||||
self.comment = module.params['comment']
|
||||
self.changed = False
|
||||
self.check_mode = module.check_mode
|
||||
self.privatekey = None
|
||||
self.fingerprint = {}
|
||||
self.public_key = {}
|
||||
self.regenerate = module.params['regenerate']
|
||||
if self.regenerate == 'always':
|
||||
self.force = True
|
||||
|
||||
if self.type in ('rsa', 'rsa1'):
|
||||
self.size = 4096 if self.size is None else self.size
|
||||
if self.size < 1024:
|
||||
module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. '
|
||||
'Attempting to use bit lengths under 1024 will cause the module to fail.'))
|
||||
|
||||
if self.type == 'dsa':
|
||||
self.size = 1024 if self.size is None else self.size
|
||||
if self.size != 1024:
|
||||
module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.'))
|
||||
|
||||
if self.type == 'ecdsa':
|
||||
self.size = 256 if self.size is None else self.size
|
||||
if self.size not in (256, 384, 521):
|
||||
module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from '
|
||||
'one of three elliptic curve sizes: 256, 384 or 521 bits. '
|
||||
'Attempting to use bit lengths other than these three values for '
|
||||
'ECDSA keys will cause this module to fail. '))
|
||||
if self.type == 'ed25519':
|
||||
self.size = 256
|
||||
|
||||
def generate(self, module):
|
||||
# generate a keypair
|
||||
if self.force or not self.isPrivateKeyValid(module, perms_required=False):
|
||||
args = [
|
||||
module.get_bin_path('ssh-keygen', True),
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(self.size),
|
||||
'-t', self.type,
|
||||
'-f', self.path,
|
||||
]
|
||||
|
||||
if self.comment:
|
||||
args.extend(['-C', self.comment])
|
||||
else:
|
||||
args.extend(['-C', ""])
|
||||
|
||||
try:
|
||||
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
|
||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
self.changed = True
|
||||
stdin_data = None
|
||||
if os.path.exists(self.path):
|
||||
stdin_data = 'y'
|
||||
module.run_command(args, data=stdin_data)
|
||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
|
||||
self.fingerprint = proc[1].split()
|
||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
||||
self.public_key = pubkey[1].strip('\n')
|
||||
except Exception as e:
|
||||
self.remove()
|
||||
module.fail_json(msg="%s" % to_native(e))
|
||||
|
||||
elif not self.isPublicKeyValid(module, perms_required=False):
|
||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
||||
pubkey = pubkey[1].strip('\n')
|
||||
try:
|
||||
self.changed = True
|
||||
with open(self.path + ".pub", "w") as pubkey_f:
|
||||
pubkey_f.write(pubkey + '\n')
|
||||
os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH)
|
||||
except IOError:
|
||||
module.fail_json(
|
||||
msg='The public key is missing or does not match the private key. '
|
||||
'Unable to regenerate the public key.')
|
||||
self.public_key = pubkey
|
||||
|
||||
if self.comment:
|
||||
try:
|
||||
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
|
||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
args = [module.get_bin_path('ssh-keygen', True),
|
||||
'-q', '-o', '-c', '-C', self.comment, '-f', self.path]
|
||||
module.run_command(args)
|
||||
except IOError:
|
||||
module.fail_json(
|
||||
msg='Unable to update the comment for the public key.')
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
file_args['path'] = file_args['path'] + '.pub'
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
|
||||
def _check_pass_protected_or_broken_key(self, module):
|
||||
key_state = module.run_command([module.get_bin_path('ssh-keygen', True),
|
||||
'-P', '', '-yf', self.path], check_rc=False)
|
||||
if key_state[0] == 255 or 'is not a public key file' in key_state[2]:
|
||||
return True
|
||||
if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def isPrivateKeyValid(self, module, perms_required=True):
|
||||
|
||||
# check if the key is correct
|
||||
def _check_state():
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if not _check_state():
|
||||
return False
|
||||
|
||||
if self._check_pass_protected_or_broken_key(module):
|
||||
if self.regenerate in ('full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False)
|
||||
if not proc[0] == 0:
|
||||
if os.path.isdir(self.path):
|
||||
module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path))
|
||||
|
||||
if self.regenerate in ('full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
fingerprint = proc[1].split()
|
||||
keysize = int(fingerprint[0])
|
||||
keytype = fingerprint[-1][1:-1].lower()
|
||||
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
if self.regenerate == 'never':
|
||||
return True
|
||||
|
||||
def _check_type():
|
||||
return self.type == keytype
|
||||
|
||||
def _check_size():
|
||||
return self.size == keysize
|
||||
|
||||
if not (_check_type() and _check_size()):
|
||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Key has wrong type and/or size.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
return not perms_required or _check_perms(module)
|
||||
|
||||
def isPublicKeyValid(self, module, perms_required=True):
|
||||
|
||||
def _get_pubkey_content():
|
||||
if os.path.exists(self.path + ".pub"):
|
||||
with open(self.path + ".pub", "r") as pubkey_f:
|
||||
present_pubkey = pubkey_f.read().strip(' \n')
|
||||
return present_pubkey
|
||||
else:
|
||||
return False
|
||||
|
||||
def _parse_pubkey(pubkey_content):
|
||||
if pubkey_content:
|
||||
parts = pubkey_content.split(' ', 2)
|
||||
if len(parts) < 2:
|
||||
return False
|
||||
return parts[0], parts[1], '' if len(parts) <= 2 else parts[2]
|
||||
return False
|
||||
|
||||
def _pubkey_valid(pubkey):
|
||||
if pubkey_parts and _parse_pubkey(pubkey):
|
||||
return pubkey_parts[:2] == _parse_pubkey(pubkey)[:2]
|
||||
return False
|
||||
|
||||
def _comment_valid():
|
||||
if pubkey_parts:
|
||||
return pubkey_parts[2] == self.comment
|
||||
return False
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
file_args['path'] = file_args['path'] + '.pub'
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
pubkey_parts = _parse_pubkey(_get_pubkey_content())
|
||||
|
||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
||||
pubkey = pubkey[1].strip('\n')
|
||||
if _pubkey_valid(pubkey):
|
||||
self.public_key = pubkey
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.comment:
|
||||
if not _comment_valid():
|
||||
return False
|
||||
|
||||
if perms_required:
|
||||
if not _check_perms(module):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def dump(self):
|
||||
# return result as a dict
|
||||
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = {
|
||||
'changed': self.changed,
|
||||
'size': self.size,
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
# On removal this has no value
|
||||
'fingerprint': self.fingerprint[1] if self.fingerprint else '',
|
||||
'public_key': self.public_key,
|
||||
'comment': self.comment if self.comment else '',
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def remove(self):
|
||||
"""Remove the resource from the filesystem."""
|
||||
|
||||
try:
|
||||
os.remove(self.path)
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise KeypairError(exc)
|
||||
else:
|
||||
pass
|
||||
|
||||
if os.path.exists(self.path + ".pub"):
|
||||
try:
|
||||
os.remove(self.path + ".pub")
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise KeypairError(exc)
|
||||
else:
|
||||
pass
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.keypair_backend import (
|
||||
select_backend
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# Define Ansible Module
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
@@ -437,49 +208,17 @@ def main():
|
||||
default='partial_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
private_key_format=dict(type='str', default='auto', no_log=False, choices=['auto']),
|
||||
backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'opensshbin'])
|
||||
),
|
||||
supports_check_mode=True,
|
||||
add_file_common_args=True,
|
||||
)
|
||||
|
||||
# Check if Path exists
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
keypair = select_backend(module, module.params['backend'])[1]
|
||||
|
||||
keypair = Keypair(module)
|
||||
|
||||
if keypair.state == 'present':
|
||||
|
||||
if module.check_mode:
|
||||
result = keypair.dump()
|
||||
result['changed'] = keypair.force or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module)
|
||||
module.exit_json(**result)
|
||||
|
||||
try:
|
||||
keypair.generate(module)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
else:
|
||||
|
||||
if module.check_mode:
|
||||
keypair.changed = os.path.exists(module.params['path'])
|
||||
if keypair.changed:
|
||||
keypair.fingerprint = {}
|
||||
result = keypair.dump()
|
||||
module.exit_json(**result)
|
||||
|
||||
try:
|
||||
keypair.remove()
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
result = keypair.dump()
|
||||
|
||||
module.exit_json(**result)
|
||||
keypair.execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ options:
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_csr_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -84,7 +85,7 @@ basic_constraints:
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[CA:TRUE, pathlen:1]"
|
||||
sample: ['CA:TRUE', 'pathlen:1']
|
||||
basic_constraints_critical:
|
||||
description: Whether the C(basic_constraints) extension is critical.
|
||||
returned: success
|
||||
@@ -94,7 +95,7 @@ extended_key_usage:
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[Biometric Info, DVCS, Time Stamping]"
|
||||
sample: [Biometric Info, DVCS, Time Stamping]
|
||||
extended_key_usage_critical:
|
||||
description: Whether the C(extended_key_usage) extension is critical.
|
||||
returned: success
|
||||
@@ -113,12 +114,12 @@ extensions_by_oid:
|
||||
returned: success
|
||||
type: str
|
||||
sample: "MAMCAQU="
|
||||
sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}'
|
||||
sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}
|
||||
key_usage:
|
||||
description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "[Key Agreement, Data Encipherment]"
|
||||
sample: [Key Agreement, Data Encipherment]
|
||||
key_usage_critical:
|
||||
description: Whether the C(key_usage) extension is critical.
|
||||
returned: success
|
||||
@@ -128,7 +129,7 @@ subject_alt_name:
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
||||
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
|
||||
subject_alt_name_critical:
|
||||
description: Whether the C(subject_alt_name) extension is critical.
|
||||
returned: success
|
||||
@@ -141,24 +142,118 @@ ocsp_must_staple_critical:
|
||||
description: Whether the C(ocsp_must_staple) extension is critical.
|
||||
returned: success
|
||||
type: bool
|
||||
name_constraints_permitted:
|
||||
description: List of permitted subtrees to sign certificates for.
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['email:.somedomain.com']
|
||||
version_added: 1.1.0
|
||||
name_constraints_excluded:
|
||||
description:
|
||||
- List of excluded subtrees the CA cannot sign certificates for.
|
||||
- Is C(none) if extension is not present.
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['email:.com']
|
||||
version_added: 1.1.0
|
||||
name_constraints_critical:
|
||||
description:
|
||||
- Whether the C(name_constraints) extension is critical.
|
||||
- Is C(none) if extension is not present.
|
||||
returned: success
|
||||
type: bool
|
||||
version_added: 1.1.0
|
||||
subject:
|
||||
description:
|
||||
- The CSR's subject as a dictionary.
|
||||
- Note that for repeated values, only the last one will be returned.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}'
|
||||
sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
|
||||
subject_ordered:
|
||||
description: The CSR's subject as an ordered list of tuples.
|
||||
returned: success
|
||||
type: list
|
||||
elements: list
|
||||
sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]'
|
||||
sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
|
||||
public_key:
|
||||
description: CSR's public key in PEM format
|
||||
returned: success
|
||||
type: str
|
||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||
public_key_type:
|
||||
description:
|
||||
- The CSR's public key's type.
|
||||
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
||||
- Will start with C(unknown) if the key type cannot be determined.
|
||||
returned: success
|
||||
type: str
|
||||
version_added: 1.7.0
|
||||
sample: RSA
|
||||
public_key_data:
|
||||
description:
|
||||
- Public key data. Depends on the public key's type.
|
||||
returned: success
|
||||
type: dict
|
||||
version_added: 1.7.0
|
||||
contains:
|
||||
size:
|
||||
description:
|
||||
- Bit size of modulus (RSA) or prime number (DSA).
|
||||
type: int
|
||||
returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
|
||||
modulus:
|
||||
description:
|
||||
- The RSA key's modulus.
|
||||
type: int
|
||||
returned: When C(public_key_type=RSA)
|
||||
exponent:
|
||||
description:
|
||||
- The RSA key's public exponent.
|
||||
type: int
|
||||
returned: When C(public_key_type=RSA)
|
||||
p:
|
||||
description:
|
||||
- The C(p) value for DSA.
|
||||
- This is the prime modulus upon which arithmetic takes place.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA)
|
||||
q:
|
||||
description:
|
||||
- The C(q) value for DSA.
|
||||
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||
multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA)
|
||||
g:
|
||||
description:
|
||||
- The C(g) value for DSA.
|
||||
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA)
|
||||
curve:
|
||||
description:
|
||||
- The curve's name for ECC.
|
||||
type: str
|
||||
returned: When C(public_key_type=ECC)
|
||||
exponent_size:
|
||||
description:
|
||||
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||
type: int
|
||||
returned: When C(public_key_type=ECC)
|
||||
x:
|
||||
description:
|
||||
- The C(x) coordinate for the public point on the elliptic curve.
|
||||
type: int
|
||||
returned: When C(public_key_type=ECC)
|
||||
y:
|
||||
description:
|
||||
- For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||
- For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
|
||||
public_key_fingerprints:
|
||||
description:
|
||||
- Fingerprints of CSR's public key.
|
||||
@@ -190,413 +285,28 @@ authority_cert_issuer:
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
||||
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
|
||||
authority_cert_serial_number:
|
||||
description:
|
||||
- The CSR's authority cert serial number.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
type: int
|
||||
sample: '12345'
|
||||
sample: 12345
|
||||
'''
|
||||
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_certificate_request,
|
||||
get_fingerprint_of_bytes,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
|
||||
select_backend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_decode_name,
|
||||
cryptography_get_extensions_from_csr,
|
||||
cryptography_oid_to_name,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_get_extensions_from_csr,
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
|
||||
class CertificateSigningRequestInfo(OpenSSLObject):
|
||||
def __init__(self, module, backend):
|
||||
super(CertificateSigningRequestInfo, self).__init__(
|
||||
module.params['path'] or '',
|
||||
'present',
|
||||
False,
|
||||
module.check_mode,
|
||||
)
|
||||
self.backend = backend
|
||||
self.module = module
|
||||
self.content = module.params['content']
|
||||
if self.content is not None:
|
||||
self.content = self.content.encode('utf-8')
|
||||
|
||||
def generate(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_ordered(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_basic_constraints(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ocsp_must_staple(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_authority_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_all_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _is_signature_valid(self):
|
||||
pass
|
||||
|
||||
def get_info(self):
|
||||
result = dict()
|
||||
self.csr = load_certificate_request(self.path, content=self.content, backend=self.backend)
|
||||
|
||||
subject = self._get_subject_ordered()
|
||||
result['subject'] = dict()
|
||||
for k, v in subject:
|
||||
result['subject'][k] = v
|
||||
result['subject_ordered'] = subject
|
||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
||||
|
||||
result['public_key'] = self._get_public_key(binary=False)
|
||||
pk = self._get_public_key(binary=True)
|
||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
|
||||
result['extensions_by_oid'] = self._get_all_extensions()
|
||||
|
||||
result['signature_valid'] = self._is_signature_valid()
|
||||
if not result['signature_valid']:
|
||||
self.module.fail_json(
|
||||
msg='CSR signature is invalid!',
|
||||
**result
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo):
|
||||
"""Validate the supplied CSR, using the cryptography backend"""
|
||||
def __init__(self, module):
|
||||
super(CertificateSigningRequestInfoCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
result = []
|
||||
for attribute in self.csr.subject:
|
||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
return result
|
||||
|
||||
def _get_key_usage(self):
|
||||
try:
|
||||
current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
|
||||
current_key_usage = current_key_ext.value
|
||||
key_usage = dict(
|
||||
digital_signature=current_key_usage.digital_signature,
|
||||
content_commitment=current_key_usage.content_commitment,
|
||||
key_encipherment=current_key_usage.key_encipherment,
|
||||
data_encipherment=current_key_usage.data_encipherment,
|
||||
key_agreement=current_key_usage.key_agreement,
|
||||
key_cert_sign=current_key_usage.key_cert_sign,
|
||||
crl_sign=current_key_usage.crl_sign,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
)
|
||||
if key_usage['key_agreement']:
|
||||
key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usage_names = dict(
|
||||
digital_signature='Digital Signature',
|
||||
content_commitment='Non Repudiation',
|
||||
key_encipherment='Key Encipherment',
|
||||
data_encipherment='Data Encipherment',
|
||||
key_agreement='Key Agreement',
|
||||
key_cert_sign='Certificate Sign',
|
||||
crl_sign='CRL Sign',
|
||||
encipher_only='Encipher Only',
|
||||
decipher_only='Decipher Only',
|
||||
)
|
||||
return sorted([
|
||||
key_usage_names[name] for name, value in key_usage.items() if value
|
||||
]), current_key_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
||||
return sorted([
|
||||
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
||||
]), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints)
|
||||
result = []
|
||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
||||
if ext_keyusage_ext.value.path_length is not None:
|
||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
||||
return sorted(result), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
try:
|
||||
try:
|
||||
# This only works with cryptography >= 2.1
|
||||
tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature)
|
||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||
except AttributeError as dummy:
|
||||
# Fallback for cryptography < 2.1
|
||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||
tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid)
|
||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
||||
return value, tlsfeature_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
try:
|
||||
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
||||
return result, san_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
return self.csr.public_key().public_bytes(
|
||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
try:
|
||||
ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
return ext.value.digest
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
try:
|
||||
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||
issuer = None
|
||||
if ext.value.authority_cert_issuer is not None:
|
||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, None, None
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return cryptography_get_extensions_from_csr(self.csr)
|
||||
|
||||
def _is_signature_valid(self):
|
||||
return self.csr.is_signature_valid
|
||||
|
||||
|
||||
class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo):
|
||||
"""validate the supplied CSR."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(CertificateSigningRequestInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def __get_name(self, name):
|
||||
result = []
|
||||
for sub in name.get_components():
|
||||
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
||||
return result
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
return self.__get_name(self.csr.get_subject())
|
||||
|
||||
def _get_extension(self, short_name):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == short_name:
|
||||
result = [
|
||||
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
||||
]
|
||||
return sorted(result), bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_key_usage(self):
|
||||
return self._get_extension(b'keyUsage')
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
return self._get_extension(b'extendedKeyUsage')
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
return self._get_extension(b'basicConstraints')
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
extensions = self.csr.get_extensions()
|
||||
oms_ext = [
|
||||
ext for ext in extensions
|
||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
||||
]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if oms_ext:
|
||||
return True, bool(oms_ext[0].get_critical())
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
return result, bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.csr.get_pubkey()
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.csr.get_pubkey()._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None, None, None
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return pyopenssl_get_extensions_from_csr(self.csr)
|
||||
|
||||
def _is_signature_valid(self):
|
||||
try:
|
||||
return bool(self.csr.verify(self.csr.get_pubkey()))
|
||||
except crypto.Error:
|
||||
# OpenSSL error means that key is not consistent
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
@@ -614,53 +324,19 @@ def main():
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if module.params['content'] is not None:
|
||||
data = module.params['content'].encode('utf-8')
|
||||
else:
|
||||
try:
|
||||
with open(module.params['path'], 'rb') as f:
|
||||
data = f.read()
|
||||
except (IOError, OSError) as e:
|
||||
module.fail_json(msg='Error while reading CSR file from disk: {0}'.format(e))
|
||||
|
||||
backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data, validate_signature=True)
|
||||
|
||||
try:
|
||||
if module.params['path'] is not None:
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detect what backend we can use
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# If cryptography is available we'll use it
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Fail if no backend has been found
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
certificate = CertificateSigningRequestInfoPyOpenSSL(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
certificate = CertificateSigningRequestInfoCryptography(module)
|
||||
|
||||
result = certificate.get_info()
|
||||
result = module_backend.get_info()
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
176
plugins/modules/openssl_csr_pipe.py
Normal file
176
plugins/modules/openssl_csr_pipe.py
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: openssl_csr_pipe
|
||||
short_description: Generate OpenSSL Certificate Signing Request (CSR)
|
||||
version_added: 1.3.0
|
||||
description:
|
||||
- "Please note that the module regenerates an existing CSR if it doesn't match the module's
|
||||
options, or if it seems to be corrupt."
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
- Felix Fontein (@felixfontein)
|
||||
options:
|
||||
content:
|
||||
description:
|
||||
- The existing CSR.
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.crypto.module_csr
|
||||
seealso:
|
||||
- module: community.crypto.openssl_csr
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Generate an OpenSSL Certificate Signing Request
|
||||
community.crypto.openssl_csr_pipe:
|
||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
||||
common_name: www.ansible.com
|
||||
register: result
|
||||
- debug:
|
||||
var: result.csr
|
||||
|
||||
- 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') }}"
|
||||
privatekey_content: "{{ private_key_content }}"
|
||||
common_name: www.ansible.com
|
||||
register: result
|
||||
- name: Store CSR
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/ssl/csr/www.ansible.com.csr
|
||||
content: "{{ result.csr }}"
|
||||
when: result is changed
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
privatekey:
|
||||
description:
|
||||
- Path to the TLS/SSL private key the CSR was generated for
|
||||
- Will be C(none) if the private key has been provided in I(privatekey_content).
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /etc/ssl/private/ansible.com.pem
|
||||
subject:
|
||||
description: A list of the subject tuples attached to the CSR
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: list
|
||||
sample: [['CN', 'www.ansible.com'], ['O', 'Ansible']]
|
||||
subjectAltName:
|
||||
description: The alternative names this CSR is valid for
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ]
|
||||
keyUsage:
|
||||
description: Purpose for which the public key may be used
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ 'digitalSignature', 'keyAgreement' ]
|
||||
extendedKeyUsage:
|
||||
description: Additional restriction on the public key purposes
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ 'clientAuth' ]
|
||||
basicConstraints:
|
||||
description: Indicates if the certificate belongs to a CA
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['CA:TRUE', 'pathLenConstraint:0']
|
||||
ocsp_must_staple:
|
||||
description: Indicates whether the certificate has the OCSP
|
||||
Must Staple feature enabled
|
||||
returned: changed or success
|
||||
type: bool
|
||||
sample: false
|
||||
name_constraints_permitted:
|
||||
description: List of permitted subtrees to sign certificates for.
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['email:.somedomain.com']
|
||||
name_constraints_excluded:
|
||||
description: List of excluded subtrees the CA cannot sign certificates for.
|
||||
returned: changed or success
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['email:.com']
|
||||
csr:
|
||||
description: The (current or generated) CSR's content.
|
||||
returned: changed or success
|
||||
type: str
|
||||
'''
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
|
||||
select_backend,
|
||||
get_csr_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
|
||||
class CertificateSigningRequestModule(object):
|
||||
def __init__(self, module, module_backend):
|
||||
self.check_mode = module.check_mode
|
||||
self.module_backend = module_backend
|
||||
self.changed = False
|
||||
if module.params['content'] is not None:
|
||||
self.module_backend.set_existing(module.params['content'].encode('utf-8'))
|
||||
|
||||
def generate(self, module):
|
||||
'''Generate the certificate signing request.'''
|
||||
if self.module_backend.needs_regeneration():
|
||||
if not self.check_mode:
|
||||
self.module_backend.generate_csr()
|
||||
self.changed = True
|
||||
|
||||
def dump(self):
|
||||
'''Serialize the object into a dictionary.'''
|
||||
result = self.module_backend.dump(include_csr=True)
|
||||
result.update({
|
||||
'changed': self.changed,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = get_csr_argument_spec()
|
||||
argument_spec.argument_spec.update(dict(
|
||||
content=dict(type='str'),
|
||||
))
|
||||
module = argument_spec.create_ansible_module(
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
try:
|
||||
backend = module.params['select_crypto_backend']
|
||||
backend, module_backend = select_backend(module, backend)
|
||||
|
||||
csr = CertificateSigningRequestModule(module, module_backend)
|
||||
csr.generate(module)
|
||||
result = csr.dump()
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,7 +15,7 @@ short_description: Generate OpenSSL Diffie-Hellman Parameters
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL DH-params.
|
||||
- This module uses file common arguments to specify generated file permissions.
|
||||
- "Please note that the module regenerates existing DH params if they don't
|
||||
- "Please note that the module regenerates existing DH params if they do not
|
||||
match the module's options. If you are concerned that this could overwrite
|
||||
your existing DH params, consider using the I(backup) option."
|
||||
- The module can use the cryptography Python library, or the C(openssl) executable.
|
||||
@@ -71,6 +71,8 @@ options:
|
||||
type: bool
|
||||
default: no
|
||||
version_added: "1.0.0"
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
seealso:
|
||||
@@ -126,10 +128,10 @@ import re
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
@@ -219,9 +221,9 @@ class DHParameterBase(object):
|
||||
def _check_fs_attributes(self, module):
|
||||
"""Checks (and changes if not in check mode!) fs attributes"""
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
attrs_changed = module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
return not attrs_changed
|
||||
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||
return False
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
@@ -331,7 +333,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
|
||||
|
||||
@@ -16,8 +16,14 @@ author:
|
||||
short_description: Generate OpenSSL PKCS#12 archive
|
||||
description:
|
||||
- This module allows one to (re-)generate PKCS#12.
|
||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available, assuming none of the
|
||||
I(iter_size) and I(maciter_size) options are used. This can be overridden with the
|
||||
I(select_crypto_backend) option.
|
||||
# Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0,
|
||||
# and will be removed in community.crypto (x+1).0.0.
|
||||
requirements:
|
||||
- python-pyOpenSSL
|
||||
- PyOpenSSL >= 0.15, < 23.3.0 or cryptography >= 3.0
|
||||
options:
|
||||
action:
|
||||
description:
|
||||
@@ -27,10 +33,19 @@ options:
|
||||
choices: [ export, parse ]
|
||||
other_certificates:
|
||||
description:
|
||||
- List of other certificates to include. Pre 2.8 this parameter was called C(ca_certificates)
|
||||
- List of other certificates to include. Pre Ansible 2.8 this parameter was called I(ca_certificates).
|
||||
- Assumes there is one PEM-encoded certificate per file. If a file contains multiple PEM certificates,
|
||||
set I(other_certificates_parse_all) to C(true).
|
||||
type: list
|
||||
elements: path
|
||||
aliases: [ ca_certificates ]
|
||||
other_certificates_parse_all:
|
||||
description:
|
||||
- If set to C(true), assumes that the files mentioned in I(other_certificates) can contain more than one
|
||||
certificate per file (or even none per file).
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 1.4.0
|
||||
certificate_path:
|
||||
description:
|
||||
- The path to read certificates and private keys from.
|
||||
@@ -49,16 +64,21 @@ options:
|
||||
iter_size:
|
||||
description:
|
||||
- Number of times to repeat the encryption step.
|
||||
- This is not considered during idempotency checks.
|
||||
- This is only used by the C(pyopenssl) backend. When using it, the default is C(2048).
|
||||
type: int
|
||||
default: 2048
|
||||
maciter_size:
|
||||
description:
|
||||
- Number of times to repeat the MAC step.
|
||||
- This is not considered during idempotency checks.
|
||||
- This is only used by the C(pyopenssl) backend. When using it, the default is C(1).
|
||||
type: int
|
||||
default: 1
|
||||
passphrase:
|
||||
description:
|
||||
- The PKCS#12 password.
|
||||
- "B(Note:) PKCS12 encryption is not secure and should not be used as a security mechanism.
|
||||
If you need to store or send a PKCS12 file safely, you should additionally encrypt it
|
||||
with something else."
|
||||
type: str
|
||||
path:
|
||||
description:
|
||||
@@ -96,6 +116,21 @@ options:
|
||||
type: bool
|
||||
default: no
|
||||
version_added: "1.0.0"
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
If one of I(iter_size) or I(maciter_size) is used, C(auto) will always result in C(pyopenssl) to be chosen
|
||||
for backwards compatibility.
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
# - Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0, and will be
|
||||
# removed in community.crypto (x+1).0.0.
|
||||
# From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
version_added: 1.7.0
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
seealso:
|
||||
@@ -115,6 +150,27 @@ EXAMPLES = r'''
|
||||
privatekey_path: /opt/certs/keys/key.pem
|
||||
certificate_path: /opt/certs/cert.pem
|
||||
other_certificates: /opt/certs/ca.pem
|
||||
# Note that if /opt/certs/ca.pem contains multiple certificates,
|
||||
# only the first one will be used. See the other_certificates_parse_all
|
||||
# option for changing this behavior.
|
||||
state: present
|
||||
|
||||
- name: Generate PKCS#12 file
|
||||
community.crypto.openssl_pkcs12:
|
||||
action: export
|
||||
path: /opt/certs/ansible.p12
|
||||
friendly_name: raclette
|
||||
privatekey_path: /opt/certs/keys/key.pem
|
||||
certificate_path: /opt/certs/cert.pem
|
||||
other_certificates_parse_all: true
|
||||
other_certificates:
|
||||
- /opt/certs/ca_bundle.pem
|
||||
# Since we set other_certificates_parse_all to true, all
|
||||
# certificates in the CA bundle are included and not just
|
||||
# the first one.
|
||||
- /opt/certs/intermediate.pem
|
||||
# In case this file has multiple certificates in it,
|
||||
# all will be included as well.
|
||||
state: present
|
||||
|
||||
- name: Change PKCS#12 file permission
|
||||
@@ -177,13 +233,16 @@ pkcs12:
|
||||
version_added: "1.0.0"
|
||||
'''
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import os
|
||||
import stat
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes, to_native
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
@@ -195,20 +254,56 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
parse_pkcs12,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_privatekey,
|
||||
load_certificate,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
|
||||
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
|
||||
except ImportError:
|
||||
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()
|
||||
pyopenssl_found = False
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
pyopenssl_found = True
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
def load_certificate_set(filename, backend):
|
||||
'''
|
||||
Load list of concatenated PEM files, and return a list of parsed certificates.
|
||||
'''
|
||||
with open(filename, 'rb') as f:
|
||||
data = f.read().decode('utf-8')
|
||||
return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)]
|
||||
|
||||
|
||||
class PkcsError(OpenSSLObjectError):
|
||||
@@ -216,20 +311,21 @@ class PkcsError(OpenSSLObjectError):
|
||||
|
||||
|
||||
class Pkcs(OpenSSLObject):
|
||||
|
||||
def __init__(self, module):
|
||||
def __init__(self, module, backend):
|
||||
super(Pkcs, self).__init__(
|
||||
module.params['path'],
|
||||
module.params['state'],
|
||||
module.params['force'],
|
||||
module.check_mode
|
||||
)
|
||||
self.backend = backend
|
||||
self.action = module.params['action']
|
||||
self.other_certificates = module.params['other_certificates']
|
||||
self.other_certificates_parse_all = module.params['other_certificates_parse_all']
|
||||
self.certificate_path = module.params['certificate_path']
|
||||
self.friendly_name = module.params['friendly_name']
|
||||
self.iter_size = module.params['iter_size']
|
||||
self.maciter_size = module.params['maciter_size']
|
||||
self.iter_size = module.params['iter_size'] or 2048
|
||||
self.maciter_size = module.params['maciter_size'] or 1
|
||||
self.passphrase = module.params['passphrase']
|
||||
self.pkcs12 = None
|
||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||
@@ -244,6 +340,42 @@ class Pkcs(OpenSSLObject):
|
||||
self.backup = module.params['backup']
|
||||
self.backup_file = None
|
||||
|
||||
if self.other_certificates:
|
||||
if self.other_certificates_parse_all:
|
||||
filenames = list(self.other_certificates)
|
||||
self.other_certificates = []
|
||||
for other_cert_bundle in filenames:
|
||||
self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend))
|
||||
else:
|
||||
self.other_certificates = [
|
||||
load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates
|
||||
]
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_bytes(self, module):
|
||||
"""Generate PKCS#12 file archive."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def parse_bytes(self, pkcs12_content):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _dump_privatekey(self, pkcs12):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _dump_certificate(self, pkcs12):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _dump_other_certificates(self, pkcs12):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_friendly_name(self, pkcs12):
|
||||
pass
|
||||
|
||||
def check(self, module, perms_required=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
@@ -252,10 +384,8 @@ class Pkcs(OpenSSLObject):
|
||||
def _check_pkey_passphrase():
|
||||
if self.privatekey_passphrase:
|
||||
try:
|
||||
load_privatekey(self.privatekey_path, self.privatekey_passphrase)
|
||||
except crypto.Error:
|
||||
return False
|
||||
except OpenSSLBadPassphraseError:
|
||||
load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
|
||||
except OpenSSLObjectError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -263,32 +393,28 @@ class Pkcs(OpenSSLObject):
|
||||
return state_and_perms
|
||||
|
||||
if os.path.exists(self.path) and module.params['action'] == 'export':
|
||||
dummy = self.generate(module)
|
||||
dummy = self.generate_bytes(module)
|
||||
self.src = self.path
|
||||
try:
|
||||
pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
|
||||
except crypto.Error:
|
||||
except OpenSSLObjectError:
|
||||
return False
|
||||
if (pkcs12_privatekey is not None) and (self.privatekey_path is not None):
|
||||
expected_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
self.pkcs12.get_privatekey())
|
||||
expected_pkey = self._dump_privatekey(self.pkcs12)
|
||||
if pkcs12_privatekey != expected_pkey:
|
||||
return False
|
||||
elif bool(pkcs12_privatekey) != bool(self.privatekey_path):
|
||||
return False
|
||||
|
||||
if (pkcs12_certificate is not None) and (self.certificate_path is not None):
|
||||
|
||||
expected_cert = crypto.dump_certificate(crypto.FILETYPE_PEM,
|
||||
self.pkcs12.get_certificate())
|
||||
expected_cert = self._dump_certificate(self.pkcs12)
|
||||
if pkcs12_certificate != expected_cert:
|
||||
return False
|
||||
elif bool(pkcs12_certificate) != bool(self.certificate_path):
|
||||
return False
|
||||
|
||||
if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
|
||||
expected_other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
|
||||
other_cert) for other_cert in self.pkcs12.get_ca_certificates()]
|
||||
expected_other_certs = self._dump_other_certificates(self.pkcs12)
|
||||
if set(pkcs12_other_certificates) != set(expected_other_certs):
|
||||
return False
|
||||
elif bool(pkcs12_other_certificates) != bool(self.other_certificates):
|
||||
@@ -297,11 +423,23 @@ class Pkcs(OpenSSLObject):
|
||||
if pkcs12_privatekey:
|
||||
# This check is required because pyOpenSSL will not return a friendly name
|
||||
# if the private key is not set in the file
|
||||
if ((self.pkcs12.get_friendlyname() is not None) and (pkcs12_friendly_name is not None)):
|
||||
if self.pkcs12.get_friendlyname() != pkcs12_friendly_name:
|
||||
friendly_name = self._get_friendly_name(self.pkcs12)
|
||||
if ((friendly_name is not None) and (pkcs12_friendly_name is not None)):
|
||||
if friendly_name != pkcs12_friendly_name:
|
||||
return False
|
||||
elif bool(self.pkcs12.get_friendlyname()) != bool(pkcs12_friendly_name):
|
||||
elif bool(friendly_name) != bool(pkcs12_friendly_name):
|
||||
return False
|
||||
elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path):
|
||||
try:
|
||||
pkey, cert, other_certs, friendly_name = self.parse()
|
||||
except OpenSSLObjectError:
|
||||
return False
|
||||
expected_content = to_bytes(
|
||||
''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
|
||||
)
|
||||
dumped_content = load_file_if_exists(self.path, ignore_errors=True)
|
||||
if expected_content != dumped_content:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -324,29 +462,6 @@ class Pkcs(OpenSSLObject):
|
||||
|
||||
return result
|
||||
|
||||
def generate(self, module):
|
||||
"""Generate PKCS#12 file archive."""
|
||||
self.pkcs12 = crypto.PKCS12()
|
||||
|
||||
if self.other_certificates:
|
||||
other_certs = [load_certificate(other_cert) for other_cert
|
||||
in self.other_certificates]
|
||||
self.pkcs12.set_ca_certificates(other_certs)
|
||||
|
||||
if self.certificate_path:
|
||||
self.pkcs12.set_certificate(load_certificate(self.certificate_path))
|
||||
|
||||
if self.friendly_name:
|
||||
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
|
||||
|
||||
if self.privatekey_path:
|
||||
try:
|
||||
self.pkcs12.set_privatekey(load_privatekey(self.privatekey_path, self.privatekey_passphrase))
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PkcsError(exc)
|
||||
|
||||
return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
|
||||
|
||||
def remove(self, module):
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
@@ -358,24 +473,13 @@ class Pkcs(OpenSSLObject):
|
||||
try:
|
||||
with open(self.src, 'rb') as pkcs12_fh:
|
||||
pkcs12_content = pkcs12_fh.read()
|
||||
p12 = crypto.load_pkcs12(pkcs12_content,
|
||||
self.passphrase)
|
||||
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
p12.get_privatekey())
|
||||
crt = crypto.dump_certificate(crypto.FILETYPE_PEM,
|
||||
p12.get_certificate())
|
||||
other_certs = []
|
||||
if p12.get_ca_certificates() is not None:
|
||||
other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
|
||||
other_cert) for other_cert in p12.get_ca_certificates()]
|
||||
|
||||
friendly_name = p12.get_friendlyname()
|
||||
|
||||
return (pkey, crt, other_certs, friendly_name)
|
||||
|
||||
return self.parse_bytes(pkcs12_content)
|
||||
except IOError as exc:
|
||||
raise PkcsError(exc)
|
||||
|
||||
def generate(self):
|
||||
pass
|
||||
|
||||
def write(self, module, content, mode=None):
|
||||
"""Write the PKCS#12 file."""
|
||||
if self.backup:
|
||||
@@ -385,15 +489,207 @@ class Pkcs(OpenSSLObject):
|
||||
self.pkcs12_bytes = content
|
||||
|
||||
|
||||
class PkcsPyOpenSSL(Pkcs):
|
||||
def __init__(self, module):
|
||||
super(PkcsPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def generate_bytes(self, module):
|
||||
"""Generate PKCS#12 file archive."""
|
||||
self.pkcs12 = crypto.PKCS12()
|
||||
|
||||
if self.other_certificates:
|
||||
self.pkcs12.set_ca_certificates(self.other_certificates)
|
||||
|
||||
if self.certificate_path:
|
||||
self.pkcs12.set_certificate(load_certificate(self.certificate_path, backend=self.backend))
|
||||
|
||||
if self.friendly_name:
|
||||
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
|
||||
|
||||
if self.privatekey_path:
|
||||
try:
|
||||
self.pkcs12.set_privatekey(
|
||||
load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend))
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PkcsError(exc)
|
||||
|
||||
return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
|
||||
|
||||
def parse_bytes(self, pkcs12_content):
|
||||
try:
|
||||
p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase)
|
||||
pkey = p12.get_privatekey()
|
||||
if pkey is not None:
|
||||
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
crt = p12.get_certificate()
|
||||
if crt is not None:
|
||||
crt = crypto.dump_certificate(crypto.FILETYPE_PEM, crt)
|
||||
other_certs = []
|
||||
if p12.get_ca_certificates() is not None:
|
||||
other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
|
||||
other_cert) for other_cert in p12.get_ca_certificates()]
|
||||
|
||||
friendly_name = p12.get_friendlyname()
|
||||
|
||||
return (pkey, crt, other_certs, friendly_name)
|
||||
except crypto.Error as exc:
|
||||
raise PkcsError(exc)
|
||||
|
||||
def _dump_privatekey(self, pkcs12):
|
||||
pk = pkcs12.get_privatekey()
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None
|
||||
|
||||
def _dump_certificate(self, pkcs12):
|
||||
cert = pkcs12.get_certificate()
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None
|
||||
|
||||
def _dump_other_certificates(self, pkcs12):
|
||||
if pkcs12.get_ca_certificates() is None:
|
||||
return []
|
||||
return [
|
||||
crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
|
||||
for other_cert in pkcs12.get_ca_certificates()
|
||||
]
|
||||
|
||||
def _get_friendly_name(self, pkcs12):
|
||||
return pkcs12.get_friendlyname()
|
||||
|
||||
|
||||
class PkcsCryptography(Pkcs):
|
||||
def __init__(self, module):
|
||||
super(PkcsCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def generate_bytes(self, module):
|
||||
"""Generate PKCS#12 file archive."""
|
||||
pkey = None
|
||||
if self.privatekey_path:
|
||||
try:
|
||||
pkey = load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PkcsError(exc)
|
||||
|
||||
cert = None
|
||||
if self.certificate_path:
|
||||
cert = load_certificate(self.certificate_path, backend=self.backend)
|
||||
|
||||
friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None
|
||||
|
||||
# Store fake object which can be used to retrieve the components back
|
||||
self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name)
|
||||
|
||||
return serialize_key_and_certificates(
|
||||
friendly_name,
|
||||
pkey,
|
||||
cert,
|
||||
self.other_certificates,
|
||||
serialization.BestAvailableEncryption(to_bytes(self.passphrase))
|
||||
if self.passphrase else serialization.NoEncryption(),
|
||||
)
|
||||
|
||||
def parse_bytes(self, pkcs12_content):
|
||||
try:
|
||||
private_key, certificate, additional_certificates, friendly_name = parse_pkcs12(
|
||||
pkcs12_content, self.passphrase)
|
||||
|
||||
pkey = None
|
||||
if private_key is not None:
|
||||
pkey = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
|
||||
crt = None
|
||||
if certificate is not None:
|
||||
crt = certificate.public_bytes(serialization.Encoding.PEM)
|
||||
|
||||
other_certs = []
|
||||
if additional_certificates is not None:
|
||||
other_certs = [
|
||||
other_cert.public_bytes(serialization.Encoding.PEM)
|
||||
for other_cert in additional_certificates
|
||||
]
|
||||
|
||||
return (pkey, crt, other_certs, friendly_name)
|
||||
except ValueError as exc:
|
||||
raise PkcsError(exc)
|
||||
|
||||
# The following methods will get self.pkcs12 passed, which is computed as:
|
||||
#
|
||||
# self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name)
|
||||
|
||||
def _dump_privatekey(self, pkcs12):
|
||||
return pkcs12[0].private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
) if pkcs12[0] else None
|
||||
|
||||
def _dump_certificate(self, pkcs12):
|
||||
return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None
|
||||
|
||||
def _dump_other_certificates(self, pkcs12):
|
||||
return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]]
|
||||
|
||||
def _get_friendly_name(self, pkcs12):
|
||||
return pkcs12[3]
|
||||
|
||||
|
||||
def select_backend(module, backend):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = (
|
||||
PYOPENSSL_FOUND and
|
||||
PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) and
|
||||
PYOPENSSL_VERSION < LooseVersion(MAXIMAL_PYOPENSSL_VERSION)
|
||||
)
|
||||
|
||||
# If no restrictions are provided, first try cryptography, then pyOpenSSL
|
||||
if module.params['iter_size'] is not None or module.params['maciter_size'] is not None:
|
||||
# If iter_size or maciter_size is specified, use pyOpenSSL backend
|
||||
backend = 'pyopenssl'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1}, < {2})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION,
|
||||
MAXIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
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)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, PkcsCryptography(module)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
action=dict(type='str', default='export', choices=['export', 'parse']),
|
||||
other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']),
|
||||
other_certificates_parse_all=dict(type='bool', default=False),
|
||||
certificate_path=dict(type='path'),
|
||||
force=dict(type='bool', default=False),
|
||||
friendly_name=dict(type='str', aliases=['name']),
|
||||
iter_size=dict(type='int', default=2048),
|
||||
maciter_size=dict(type='int', default=1),
|
||||
iter_size=dict(type='int'),
|
||||
maciter_size=dict(type='int'),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
path=dict(type='path', required=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
@@ -402,6 +698,7 @@ def main():
|
||||
src=dict(type='path'),
|
||||
backup=dict(type='bool', default=False),
|
||||
return_content=dict(type='bool', default=False),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
)
|
||||
|
||||
required_if = [
|
||||
@@ -415,8 +712,7 @@ def main():
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not pyopenssl_found:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL'), exception=PYOPENSSL_IMP_ERR)
|
||||
backend, pkcs12 = select_backend(module, module.params['select_crypto_backend'])
|
||||
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
@@ -426,7 +722,6 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
pkcs12 = Pkcs(module)
|
||||
changed = False
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
@@ -439,16 +734,19 @@ def main():
|
||||
if module.params['action'] == 'export':
|
||||
if not module.params['friendly_name']:
|
||||
module.fail_json(msg='Friendly_name is required')
|
||||
pkcs12_content = pkcs12.generate(module)
|
||||
pkcs12_content = pkcs12.generate_bytes(module)
|
||||
pkcs12.write(module, pkcs12_content, 0o600)
|
||||
changed = True
|
||||
else:
|
||||
pkey, cert, other_certs, friendly_name = pkcs12.parse()
|
||||
dump_content = '%s%s%s' % (to_native(pkey), to_native(cert), to_native(b''.join(other_certs)))
|
||||
dump_content = ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
|
||||
pkcs12.write(module, to_bytes(dump_content))
|
||||
changed = True
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, changed):
|
||||
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||
changed = True
|
||||
elif module.set_fs_attributes_if_different(file_args, changed):
|
||||
changed = True
|
||||
else:
|
||||
if module.check_mode:
|
||||
|
||||
@@ -14,23 +14,7 @@ module: openssl_privatekey
|
||||
short_description: Generate OpenSSL private keys
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL private keys.
|
||||
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29),
|
||||
L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
|
||||
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or
|
||||
L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys.
|
||||
- Keys are generated in PEM format.
|
||||
- "Please note that the module regenerates private keys if they don't match
|
||||
the module's options. In particular, if you provide another passphrase
|
||||
(or specify none), change the keysize, etc., the private key will be
|
||||
regenerated. If you are concerned that this could **overwrite your private key**,
|
||||
consider using the I(backup) option."
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13."
|
||||
requirements:
|
||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Or pyOpenSSL
|
||||
- The default mode for the private key file will be C(0600) if I(mode) is not explicitly set.
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
- Felix Fontein (@felixfontein)
|
||||
@@ -41,48 +25,6 @@ options:
|
||||
type: str
|
||||
default: present
|
||||
choices: [ absent, present ]
|
||||
size:
|
||||
description:
|
||||
- Size (in bits) of the TLS/SSL key to generate.
|
||||
type: int
|
||||
default: 4096
|
||||
type:
|
||||
description:
|
||||
- The algorithm used to generate the TLS/SSL private key.
|
||||
- Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend.
|
||||
C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require
|
||||
cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the
|
||||
I(curve) option.
|
||||
type: str
|
||||
default: RSA
|
||||
choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ]
|
||||
curve:
|
||||
description:
|
||||
- Note that not all curves are supported by all versions of C(cryptography).
|
||||
- For maximal interoperability, C(secp384r1) or C(secp256r1) should be used.
|
||||
- We use the curve names as defined in the
|
||||
L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
|
||||
type: str
|
||||
choices:
|
||||
- secp384r1
|
||||
- secp521r1
|
||||
- secp224r1
|
||||
- secp192r1
|
||||
- secp256r1
|
||||
- secp256k1
|
||||
- brainpoolP256r1
|
||||
- brainpoolP384r1
|
||||
- brainpoolP512r1
|
||||
- sect571k1
|
||||
- sect409k1
|
||||
- sect283k1
|
||||
- sect233k1
|
||||
- sect163k1
|
||||
- sect571r1
|
||||
- sect409r1
|
||||
- sect283r1
|
||||
- sect233r1
|
||||
- sect163r2
|
||||
force:
|
||||
description:
|
||||
- Should the key be regenerated even if it already exists.
|
||||
@@ -90,56 +32,13 @@ options:
|
||||
default: no
|
||||
path:
|
||||
description:
|
||||
- Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode.
|
||||
- Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode
|
||||
if I(mode) is not explicitly set.
|
||||
type: path
|
||||
required: true
|
||||
passphrase:
|
||||
description:
|
||||
- The passphrase for the private key.
|
||||
type: str
|
||||
cipher:
|
||||
description:
|
||||
- The cipher to encrypt the private key. (Valid values can be found by
|
||||
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
|
||||
depending on your OpenSSL version.)
|
||||
- When using the C(cryptography) backend, use C(auto).
|
||||
type: str
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
format:
|
||||
description:
|
||||
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
|
||||
is used for all keys which support it. Please note that not every key can be exported in any format.
|
||||
- The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same,
|
||||
but for existing private key files, it will not force a regenerate when its format is not the automatically
|
||||
selected one for generation.
|
||||
- Note that if the format for an existing private key mismatches, the key is *regenerated* by default.
|
||||
To change this behavior, use the I(format_mismatch) option.
|
||||
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
|
||||
fail if a value different from C(auto_ignore) is used.
|
||||
type: str
|
||||
default: auto_ignore
|
||||
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
|
||||
version_added: '1.0.0'
|
||||
format_mismatch:
|
||||
description:
|
||||
- Determines behavior of the module if the format of a private key does not match the expected format, but all
|
||||
other parameters are as expected.
|
||||
- If set to C(regenerate) (default), generates a new private key.
|
||||
- If set to C(convert), the key will be converted to the new format instead.
|
||||
- Only supported by the C(cryptography) backend.
|
||||
type: str
|
||||
default: regenerate
|
||||
choices: [ regenerate, convert ]
|
||||
version_added: '1.0.0'
|
||||
backup:
|
||||
description:
|
||||
@@ -152,50 +51,19 @@ options:
|
||||
- If set to C(yes), will return the (current or generated) private key's content as I(privatekey).
|
||||
- Note that especially if the private key is not encrypted, you have to make sure that the returned
|
||||
value is treated appropriately and not accidentally written to logs etc.! Use with care!
|
||||
- Use Ansible's I(no_log) task option to avoid the output being shown. See also
|
||||
U(https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#how-do-i-keep-secret-data-in-my-playbook).
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '1.0.0'
|
||||
regenerate:
|
||||
description:
|
||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
||||
The module will always generate a new key if the destination file does not exist.
|
||||
- By default, the key will be regenerated when it doesn't match the module's options,
|
||||
except when the key cannot be read or the passphrase does not match. Please note that
|
||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
||||
is specified.
|
||||
- If set to C(never), the module will fail if the key cannot be read or the passphrase
|
||||
isn't matching, and will never regenerate an existing key.
|
||||
- If set to C(fail), the module will fail if the key does not correspond to the module's
|
||||
options.
|
||||
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
|
||||
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
|
||||
the key is protected by an unknown passphrase, or when they key is not protected by a
|
||||
passphrase, but a passphrase is specified.
|
||||
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
|
||||
module's options. This is also the case if the key cannot be read (broken file), the key
|
||||
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
|
||||
but a passphrase is specified. Make sure you have a B(backup) when using this option!
|
||||
- If set to C(always), the module will always regenerate the key. This is equivalent to
|
||||
setting I(force) to C(yes).
|
||||
- Note that if I(format_mismatch) is set to C(convert) and everything matches except the
|
||||
format, the key will always be converted, except if I(regenerate) is set to C(always).
|
||||
type: str
|
||||
choices:
|
||||
- never
|
||||
- fail
|
||||
- partial_idempotence
|
||||
- full_idempotence
|
||||
- always
|
||||
default: full_idempotence
|
||||
version_added: '1.0.0'
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
- ansible.builtin.files
|
||||
- community.crypto.module_privatekey
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_dhparam
|
||||
- module: community.crypto.openssl_pkcs12
|
||||
- module: community.crypto.openssl_publickey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
- module: community.crypto.openssl_privatekey_info
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -273,15 +141,10 @@ privatekey:
|
||||
version_added: '1.0.0'
|
||||
'''
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
@@ -289,83 +152,32 @@ from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_privatekey,
|
||||
get_fingerprint,
|
||||
get_fingerprint_of_bytes,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||
identify_private_key_format,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import (
|
||||
select_backend,
|
||||
get_privatekey_argument_spec,
|
||||
)
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
class PrivateKeyModule(OpenSSLObject):
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.exceptions
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
import cryptography.hazmat.primitives.asymmetric.utils
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
class PrivateKeyError(OpenSSLObjectError):
|
||||
pass
|
||||
|
||||
|
||||
class PrivateKeyBase(OpenSSLObject):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyBase, self).__init__(
|
||||
def __init__(self, module, module_backend):
|
||||
super(PrivateKeyModule, self).__init__(
|
||||
module.params['path'],
|
||||
module.params['state'],
|
||||
module.params['force'],
|
||||
module.check_mode
|
||||
module.check_mode,
|
||||
)
|
||||
self.size = module.params['size']
|
||||
self.passphrase = module.params['passphrase']
|
||||
self.cipher = module.params['cipher']
|
||||
self.privatekey = None
|
||||
self.fingerprint = {}
|
||||
self.format = module.params['format']
|
||||
self.format_mismatch = module.params['format_mismatch']
|
||||
self.privatekey_bytes = None
|
||||
self.module_backend = module_backend
|
||||
self.return_content = module.params['return_content']
|
||||
self.regenerate = module.params['regenerate']
|
||||
if self.regenerate == 'always':
|
||||
self.force = True
|
||||
if self.force:
|
||||
module_backend.regenerate = 'always'
|
||||
|
||||
self.backup = module.params['backup']
|
||||
self.backup_file = None
|
||||
@@ -373,510 +185,71 @@ class PrivateKeyBase(OpenSSLObject):
|
||||
if module.params['mode'] is None:
|
||||
module.params['mode'] = '0600'
|
||||
|
||||
@abc.abstractmethod
|
||||
def _generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Make sure that the private key has been loaded."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_private_key_data(self):
|
||||
"""Return bytes for self.privatekey"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_fingerprint(self):
|
||||
pass
|
||||
module_backend.set_existing(load_file_if_exists(self.path, module))
|
||||
|
||||
def generate(self, module):
|
||||
"""Generate a keypair."""
|
||||
|
||||
if not self.check(module, perms_required=False, ignore_conversion=True) or self.force:
|
||||
if self.module_backend.needs_regeneration():
|
||||
# Regenerate
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
self._generate_private_key()
|
||||
privatekey_data = self._get_private_key_data()
|
||||
if self.return_content:
|
||||
self.privatekey_bytes = privatekey_data
|
||||
write_file(module, privatekey_data, 0o600)
|
||||
if not self.check_mode:
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
self.module_backend.generate_private_key()
|
||||
privatekey_data = self.module_backend.get_private_key_data()
|
||||
if self.return_content:
|
||||
self.privatekey_bytes = privatekey_data
|
||||
write_file(module, privatekey_data, 0o600)
|
||||
self.changed = True
|
||||
elif not self.check(module, perms_required=False, ignore_conversion=False):
|
||||
elif self.module_backend.needs_conversion():
|
||||
# Convert
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
self._ensure_private_key_loaded()
|
||||
privatekey_data = self._get_private_key_data()
|
||||
if self.return_content:
|
||||
self.privatekey_bytes = privatekey_data
|
||||
write_file(module, privatekey_data, 0o600)
|
||||
if not self.check_mode:
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
self.module_backend.convert_private_key()
|
||||
privatekey_data = self.module_backend.get_private_key_data()
|
||||
if self.return_content:
|
||||
self.privatekey_bytes = privatekey_data
|
||||
write_file(module, privatekey_data, 0o600)
|
||||
self.changed = True
|
||||
|
||||
self.fingerprint = self._get_fingerprint()
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||
self.changed = True
|
||||
else:
|
||||
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
|
||||
def remove(self, module):
|
||||
if self.backup:
|
||||
self.module_backend.set_existing(None)
|
||||
if self.backup and not self.check_mode:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
super(PrivateKeyBase, self).remove(module)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_passphrase(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_size_and_type(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_format(self):
|
||||
pass
|
||||
|
||||
def check(self, module, perms_required=True, ignore_conversion=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
state_and_perms = super(PrivateKeyBase, self).check(module, perms_required=False)
|
||||
|
||||
if not state_and_perms:
|
||||
# key does not exist
|
||||
return False
|
||||
|
||||
if not self._check_passphrase():
|
||||
if self.regenerate in ('full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
if self.regenerate != 'never':
|
||||
if not self._check_size_and_type():
|
||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Key has wrong type and/or size.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
if not self._check_format():
|
||||
# During conversion step, convert if format does not match and format_mismatch == 'convert'
|
||||
if not ignore_conversion and self.format_mismatch == 'convert':
|
||||
return False
|
||||
# During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
|
||||
if ignore_conversion and self.format_mismatch == 'regenerate' and self.regenerate != 'never':
|
||||
if not ignore_conversion or self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Key has wrong format.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.'
|
||||
' To convert the key, set `format_mismatch` to `convert`.')
|
||||
|
||||
# check whether permissions are correct (in case that needs to be checked)
|
||||
return not perms_required or super(PrivateKeyBase, self).check(module, perms_required=perms_required)
|
||||
super(PrivateKeyModule, self).remove(module)
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
result = {
|
||||
'size': self.size,
|
||||
'filename': self.path,
|
||||
'changed': self.changed,
|
||||
'fingerprint': self.fingerprint,
|
||||
}
|
||||
result = self.module_backend.dump(include_key=self.return_content)
|
||||
result['filename'] = self.path
|
||||
result['changed'] = self.changed
|
||||
if self.backup_file:
|
||||
result['backup_file'] = self.backup_file
|
||||
if self.return_content:
|
||||
if self.privatekey_bytes is None:
|
||||
self.privatekey_bytes = load_file_if_exists(self.path, ignore_errors=True)
|
||||
if self.privatekey_bytes:
|
||||
if identify_private_key_format(self.privatekey_bytes) == 'raw':
|
||||
result['privatekey'] = base64.b64encode(self.privatekey_bytes)
|
||||
else:
|
||||
result['privatekey'] = self.privatekey_bytes.decode('utf-8')
|
||||
else:
|
||||
result['privatekey'] = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class PrivateKeyPyOpenSSL(PrivateKeyBase):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyPyOpenSSL, self).__init__(module)
|
||||
|
||||
if module.params['type'] == 'RSA':
|
||||
self.type = crypto.TYPE_RSA
|
||||
elif module.params['type'] == 'DSA':
|
||||
self.type = crypto.TYPE_DSA
|
||||
else:
|
||||
module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
|
||||
|
||||
if self.format != 'auto_ignore':
|
||||
module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
|
||||
|
||||
def _generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
self.privatekey = crypto.PKey()
|
||||
try:
|
||||
self.privatekey.generate_key(self.type, self.size)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Make sure that the private key has been loaded."""
|
||||
if self.privatekey is None:
|
||||
try:
|
||||
self.privatekey = privatekey = load_privatekey(self.path, self.passphrase)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _get_private_key_data(self):
|
||||
"""Return bytes for self.privatekey"""
|
||||
if self.cipher and self.passphrase:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey,
|
||||
self.cipher, to_bytes(self.passphrase))
|
||||
else:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey)
|
||||
|
||||
def _get_fingerprint(self):
|
||||
return get_fingerprint(self.path, self.passphrase)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
load_privatekey(self.path, self.passphrase)
|
||||
return True
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
def _check_size(privatekey):
|
||||
return self.size == privatekey.bits()
|
||||
|
||||
def _check_type(privatekey):
|
||||
return self.type == privatekey.type()
|
||||
|
||||
self._ensure_private_key_loaded()
|
||||
return _check_size(self.privatekey) and _check_type(self.privatekey)
|
||||
|
||||
def _check_format(self):
|
||||
# Not supported by this backend
|
||||
return True
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
result = super(PrivateKeyPyOpenSSL, self).dump()
|
||||
|
||||
if self.type == crypto.TYPE_RSA:
|
||||
result['type'] = 'RSA'
|
||||
else:
|
||||
result['type'] = 'DSA'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class PrivateKeyCryptography(PrivateKeyBase):
|
||||
|
||||
def _get_ec_class(self, ectype):
|
||||
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype)
|
||||
if ecclass is None:
|
||||
self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype))
|
||||
return ecclass
|
||||
|
||||
def _add_curve(self, name, ectype, deprecated=False):
|
||||
def create(size):
|
||||
ecclass = self._get_ec_class(ectype)
|
||||
return ecclass()
|
||||
|
||||
def verify(privatekey):
|
||||
ecclass = self._get_ec_class(ectype)
|
||||
return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass)
|
||||
|
||||
self.curves[name] = {
|
||||
'create': create,
|
||||
'verify': verify,
|
||||
'deprecated': deprecated,
|
||||
}
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyCryptography, self).__init__(module)
|
||||
|
||||
self.curves = dict()
|
||||
self._add_curve('secp384r1', 'SECP384R1')
|
||||
self._add_curve('secp521r1', 'SECP521R1')
|
||||
self._add_curve('secp224r1', 'SECP224R1')
|
||||
self._add_curve('secp192r1', 'SECP192R1')
|
||||
self._add_curve('secp256r1', 'SECP256R1')
|
||||
self._add_curve('secp256k1', 'SECP256K1')
|
||||
self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True)
|
||||
self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True)
|
||||
self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True)
|
||||
self._add_curve('sect571k1', 'SECT571K1', deprecated=True)
|
||||
self._add_curve('sect409k1', 'SECT409K1', deprecated=True)
|
||||
self._add_curve('sect283k1', 'SECT283K1', deprecated=True)
|
||||
self._add_curve('sect233k1', 'SECT233K1', deprecated=True)
|
||||
self._add_curve('sect163k1', 'SECT163K1', deprecated=True)
|
||||
self._add_curve('sect571r1', 'SECT571R1', deprecated=True)
|
||||
self._add_curve('sect409r1', 'SECT409R1', deprecated=True)
|
||||
self._add_curve('sect283r1', 'SECT283R1', deprecated=True)
|
||||
self._add_curve('sect233r1', 'SECT233R1', deprecated=True)
|
||||
self._add_curve('sect163r2', 'SECT163R2', deprecated=True)
|
||||
|
||||
self.module = module
|
||||
self.cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
|
||||
self.type = module.params['type']
|
||||
self.curve = module.params['curve']
|
||||
if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X25519')
|
||||
if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X25519 serialization')
|
||||
if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X448')
|
||||
if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support Ed25519')
|
||||
if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.module.fail_json(msg='Your cryptography version does not support Ed448')
|
||||
|
||||
def _get_wanted_format(self):
|
||||
if self.format not in ('auto', 'auto_ignore'):
|
||||
return self.format
|
||||
if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'):
|
||||
return 'pkcs8'
|
||||
else:
|
||||
return 'pkcs1'
|
||||
|
||||
def _generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
try:
|
||||
if self.type == 'RSA':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
|
||||
public_exponent=65537, # OpenSSL always uses this
|
||||
key_size=self.size,
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
if self.type == 'DSA':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
|
||||
key_size=self.size,
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
|
||||
if self.type == 'ECC' and self.curve in self.curves:
|
||||
if self.curves[self.curve]['deprecated']:
|
||||
self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve))
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
|
||||
curve=self.curves[self.curve]['create'](self.size),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
|
||||
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Make sure that the private key has been loaded."""
|
||||
if self.privatekey is None:
|
||||
self.privatekey = self._load_privatekey()
|
||||
|
||||
def _get_private_key_data(self):
|
||||
"""Return bytes for self.privatekey"""
|
||||
# Select export format and encoding
|
||||
try:
|
||||
export_format = self._get_wanted_format()
|
||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
|
||||
if export_format == 'pkcs1':
|
||||
# "TraditionalOpenSSL" format is PKCS1
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
|
||||
elif export_format == 'pkcs8':
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
elif export_format == 'raw':
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
|
||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw
|
||||
except AttributeError:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
|
||||
|
||||
# Select key encryption
|
||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
|
||||
if self.cipher and self.passphrase:
|
||||
if self.cipher == 'auto':
|
||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase))
|
||||
else:
|
||||
self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.')
|
||||
|
||||
# Serialize key
|
||||
try:
|
||||
return self.privatekey.private_bytes(
|
||||
encoding=export_encoding,
|
||||
format=export_format,
|
||||
encryption_algorithm=encryption_algorithm
|
||||
)
|
||||
except ValueError as dummy:
|
||||
self.module.fail_json(
|
||||
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
|
||||
)
|
||||
except Exception as dummy:
|
||||
self.module.fail_json(
|
||||
msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
|
||||
exception=traceback.format_exc()
|
||||
)
|
||||
|
||||
def _load_privatekey(self):
|
||||
try:
|
||||
# Read bytes
|
||||
with open(self.path, 'rb') as f:
|
||||
data = f.read()
|
||||
# Interpret bytes depending on format.
|
||||
format = identify_private_key_format(data)
|
||||
if format == 'raw':
|
||||
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
|
||||
return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
|
||||
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
|
||||
return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
|
||||
if len(data) == 32:
|
||||
if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519):
|
||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519):
|
||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
||||
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
|
||||
try:
|
||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
||||
except Exception:
|
||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
||||
raise PrivateKeyError('Cannot load raw key')
|
||||
else:
|
||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
data,
|
||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except Exception as e:
|
||||
raise PrivateKeyError(e)
|
||||
|
||||
def _get_fingerprint(self):
|
||||
# Get bytes of public key
|
||||
private_key = self._load_privatekey()
|
||||
public_key = private_key.public_key()
|
||||
public_key_bytes = public_key.public_bytes(
|
||||
cryptography.hazmat.primitives.serialization.Encoding.DER,
|
||||
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
# Get fingerprints of public_key_bytes
|
||||
return get_fingerprint_of_bytes(public_key_bytes)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
with open(self.path, 'rb') as f:
|
||||
data = f.read()
|
||||
format = identify_private_key_format(data)
|
||||
if format == 'raw':
|
||||
# Raw keys cannot be encrypted. To avoid incompatibilities, we try to
|
||||
# actually load the key (and return False when this fails).
|
||||
self._load_privatekey()
|
||||
# Loading the key succeeded. Only return True when no passphrase was
|
||||
# provided.
|
||||
return self.passphrase is None
|
||||
else:
|
||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
data,
|
||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
return self.type == 'RSA' and self.size == self.privatekey.key_size
|
||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
return self.type == 'DSA' and self.size == self.privatekey.key_size
|
||||
if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
||||
return self.type == 'X25519'
|
||||
if CRYPTOGRAPHY_HAS_X448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
||||
return self.type == 'X448'
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
return self.type == 'Ed25519'
|
||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
return self.type == 'Ed448'
|
||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
if self.type != 'ECC':
|
||||
return False
|
||||
if self.curve not in self.curves:
|
||||
return False
|
||||
return self.curves[self.curve]['verify'](self.privatekey)
|
||||
|
||||
return False
|
||||
|
||||
def _check_format(self):
|
||||
if self.format == 'auto_ignore':
|
||||
return True
|
||||
try:
|
||||
with open(self.path, 'rb') as f:
|
||||
content = f.read()
|
||||
format = identify_private_key_format(content)
|
||||
return format == self._get_wanted_format()
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = super(PrivateKeyCryptography, self).dump()
|
||||
result['type'] = self.type
|
||||
if self.type == 'ECC':
|
||||
result['curve'] = self.curve
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
size=dict(type='int', default=4096),
|
||||
type=dict(type='str', default='RSA', choices=[
|
||||
'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448'
|
||||
]),
|
||||
curve=dict(type='str', choices=[
|
||||
'secp384r1', 'secp521r1', 'secp224r1', 'secp192r1', 'secp256r1',
|
||||
'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1',
|
||||
'sect571k1', 'sect409k1', 'sect283k1', 'sect233k1', 'sect163k1',
|
||||
'sect571r1', 'sect409r1', 'sect283r1', 'sect233r1', 'sect163r2',
|
||||
]),
|
||||
force=dict(type='bool', default=False),
|
||||
path=dict(type='path', required=True),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
cipher=dict(type='str'),
|
||||
backup=dict(type='bool', default=False),
|
||||
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
|
||||
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
return_content=dict(type='bool', default=False),
|
||||
regenerate=dict(
|
||||
type='str',
|
||||
default='full_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
),
|
||||
argument_spec = get_privatekey_argument_spec()
|
||||
argument_spec.argument_spec.update(dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
force=dict(type='bool', default=False),
|
||||
path=dict(type='path', required=True),
|
||||
backup=dict(type='bool', default=False),
|
||||
return_content=dict(type='bool', default=False),
|
||||
))
|
||||
module = argument_spec.create_ansible_module(
|
||||
supports_check_mode=True,
|
||||
add_file_common_args=True,
|
||||
required_together=[
|
||||
['cipher', 'passphrase']
|
||||
],
|
||||
required_if=[
|
||||
['type', 'ECC', ['curve']],
|
||||
],
|
||||
)
|
||||
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
@@ -886,61 +259,17 @@ def main():
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# 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)
|
||||
backend, module_backend = select_backend(
|
||||
module=module,
|
||||
backend=module.params['select_crypto_backend'],
|
||||
)
|
||||
|
||||
# Decision
|
||||
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
|
||||
# First try pyOpenSSL, then cryptography
|
||||
if can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
else:
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
try:
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
private_key = PrivateKeyPyOpenSSL(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
private_key = PrivateKeyCryptography(module)
|
||||
private_key = PrivateKeyModule(module, module_backend)
|
||||
|
||||
if private_key.state == 'present':
|
||||
if module.check_mode:
|
||||
result = private_key.dump()
|
||||
result['changed'] = private_key.force \
|
||||
or not private_key.check(module, ignore_conversion=True) \
|
||||
or not private_key.check(module, ignore_conversion=False)
|
||||
module.exit_json(**result)
|
||||
|
||||
private_key.generate(module)
|
||||
else:
|
||||
if module.check_mode:
|
||||
result = private_key.dump()
|
||||
result['changed'] = os.path.exists(module.params['path'])
|
||||
module.exit_json(**result)
|
||||
|
||||
private_key.remove(module)
|
||||
|
||||
result = private_key.dump()
|
||||
|
||||
@@ -65,8 +65,12 @@ options:
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.openssl_privatekey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -80,28 +84,31 @@ EXAMPLES = r'''
|
||||
register: result
|
||||
|
||||
- name: Dump information
|
||||
debug:
|
||||
ansible.builtin.debug:
|
||||
var: result
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
can_load_key:
|
||||
description: Whether the module was able to load the private key from disk
|
||||
description: Whether the module was able to load the private key from disk.
|
||||
returned: always
|
||||
type: bool
|
||||
can_parse_key:
|
||||
description: Whether the module was able to parse the private key
|
||||
description: Whether the module was able to parse the private key.
|
||||
returned: always
|
||||
type: bool
|
||||
key_is_consistent:
|
||||
description:
|
||||
- Whether the key is consistent. Can also return C(none) next to C(yes) and
|
||||
C(no), to indicate that consistency couldn't be checked.
|
||||
C(no), to indicate that consistency could not be checked.
|
||||
- In case the check returns C(no), the module will fail.
|
||||
- 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.
|
||||
returned: always
|
||||
type: bool
|
||||
public_key:
|
||||
description: Private key's public key in PEM format
|
||||
description: Private key's public key in PEM format.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||
@@ -126,6 +133,62 @@ public_data:
|
||||
- Public key data. Depends on key type.
|
||||
returned: success
|
||||
type: dict
|
||||
contains:
|
||||
size:
|
||||
description:
|
||||
- Bit size of modulus (RSA) or prime number (DSA).
|
||||
type: int
|
||||
returned: When C(type=RSA) or C(type=DSA)
|
||||
modulus:
|
||||
description:
|
||||
- The RSA key's modulus.
|
||||
type: int
|
||||
returned: When C(type=RSA)
|
||||
exponent:
|
||||
description:
|
||||
- The RSA key's public exponent.
|
||||
type: int
|
||||
returned: When C(type=RSA)
|
||||
p:
|
||||
description:
|
||||
- The C(p) value for DSA.
|
||||
- This is the prime modulus upon which arithmetic takes place.
|
||||
type: int
|
||||
returned: When C(type=DSA)
|
||||
q:
|
||||
description:
|
||||
- The C(q) value for DSA.
|
||||
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||
multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(type=DSA)
|
||||
g:
|
||||
description:
|
||||
- The C(g) value for DSA.
|
||||
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(type=DSA)
|
||||
curve:
|
||||
description:
|
||||
- The curve's name for ECC.
|
||||
type: str
|
||||
returned: When C(type=ECC)
|
||||
exponent_size:
|
||||
description:
|
||||
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||
type: int
|
||||
returned: When C(type=ECC)
|
||||
x:
|
||||
description:
|
||||
- The C(x) coordinate for the public point on the elliptic curve.
|
||||
type: int
|
||||
returned: When C(type=ECC)
|
||||
y:
|
||||
description:
|
||||
- For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||
- For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||
type: int
|
||||
returned: When C(type=DSA) or C(type=ECC)
|
||||
private_data:
|
||||
description:
|
||||
- Private key data. Depends on key type.
|
||||
@@ -134,456 +197,25 @@ private_data:
|
||||
'''
|
||||
|
||||
|
||||
import abc
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_privatekey,
|
||||
get_fingerprint_of_bytes,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
|
||||
PrivateKeyConsistencyError,
|
||||
PrivateKeyParseError,
|
||||
select_backend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
|
||||
binary_exp_mod,
|
||||
quick_is_not_prime,
|
||||
)
|
||||
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
SIGNATURE_TEST_DATA = b'1234'
|
||||
|
||||
|
||||
def _get_cryptography_key_info(key):
|
||||
key_public_data = dict()
|
||||
key_private_data = dict()
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
key_type = 'RSA'
|
||||
key_public_data['size'] = key.key_size
|
||||
key_public_data['modulus'] = key.public_key().public_numbers().n
|
||||
key_public_data['exponent'] = key.public_key().public_numbers().e
|
||||
key_private_data['p'] = key.private_numbers().p
|
||||
key_private_data['q'] = key.private_numbers().q
|
||||
key_private_data['exponent'] = key.private_numbers().d
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
key_type = 'DSA'
|
||||
key_public_data['size'] = key.key_size
|
||||
key_public_data['p'] = key.parameters().parameter_numbers().p
|
||||
key_public_data['q'] = key.parameters().parameter_numbers().q
|
||||
key_public_data['g'] = key.parameters().parameter_numbers().g
|
||||
key_public_data['y'] = key.public_key().public_numbers().y
|
||||
key_private_data['x'] = key.private_numbers().x
|
||||
elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
||||
key_type = 'X25519'
|
||||
elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
||||
key_type = 'X448'
|
||||
elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
key_type = 'Ed25519'
|
||||
elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
key_type = 'Ed448'
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
key_type = 'ECC'
|
||||
key_public_data['curve'] = key.public_key().curve.name
|
||||
key_public_data['x'] = key.public_key().public_numbers().x
|
||||
key_public_data['y'] = key.public_key().public_numbers().y
|
||||
key_public_data['exponent_size'] = key.public_key().curve.key_size
|
||||
key_private_data['multiplier'] = key.private_numbers().private_value
|
||||
else:
|
||||
key_type = 'unknown ({0})'.format(type(key))
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
|
||||
def _check_dsa_consistency(key_public_data, key_private_data):
|
||||
# Get parameters
|
||||
p = key_public_data.get('p')
|
||||
q = key_public_data.get('q')
|
||||
g = key_public_data.get('g')
|
||||
y = key_public_data.get('y')
|
||||
x = key_private_data.get('x')
|
||||
for v in (p, q, g, y, x):
|
||||
if v is None:
|
||||
return None
|
||||
# Make sure that g is not 0, 1 or -1 in Z/pZ
|
||||
if g < 2 or g >= p - 1:
|
||||
return False
|
||||
# Make sure that x is in range
|
||||
if x < 1 or x >= q:
|
||||
return False
|
||||
# Check whether q divides p-1
|
||||
if (p - 1) % q != 0:
|
||||
return False
|
||||
# Check that g**q mod p == 1
|
||||
if binary_exp_mod(g, q, p) != 1:
|
||||
return False
|
||||
# Check whether g**x mod p == y
|
||||
if binary_exp_mod(g, x, p) != y:
|
||||
return False
|
||||
# Check (quickly) whether p or q are not primes
|
||||
if quick_is_not_prime(q) or quick_is_not_prime(p):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
return bool(key._backend._lib.RSA_check_key(key._rsa_cdata))
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
||||
if result is not None:
|
||||
return result
|
||||
try:
|
||||
signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256())
|
||||
except AttributeError:
|
||||
# sign() was added in cryptography 1.5, but we support older versions
|
||||
return None
|
||||
try:
|
||||
key.public_key().verify(
|
||||
signature,
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.hashes.SHA256()
|
||||
)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
try:
|
||||
signature = key.sign(
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
||||
)
|
||||
except AttributeError:
|
||||
# sign() was added in cryptography 1.5, but we support older versions
|
||||
return None
|
||||
try:
|
||||
key.public_key().verify(
|
||||
signature,
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
||||
)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
has_simple_sign_function = False
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
has_simple_sign_function = True
|
||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
has_simple_sign_function = True
|
||||
if has_simple_sign_function:
|
||||
signature = key.sign(SIGNATURE_TEST_DATA)
|
||||
try:
|
||||
key.public_key().verify(signature, SIGNATURE_TEST_DATA)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
# For X25519 and X448, there's no test yet.
|
||||
return None
|
||||
|
||||
|
||||
class PrivateKeyInfo(OpenSSLObject):
|
||||
def __init__(self, module, backend):
|
||||
super(PrivateKeyInfo, self).__init__(
|
||||
module.params['path'] or '',
|
||||
'present',
|
||||
False,
|
||||
module.check_mode,
|
||||
)
|
||||
self.backend = backend
|
||||
self.module = module
|
||||
self.content = module.params['content']
|
||||
|
||||
self.passphrase = module.params['passphrase']
|
||||
self.return_private_key_data = module.params['return_private_key_data']
|
||||
|
||||
def generate(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_info(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
pass
|
||||
|
||||
def get_info(self):
|
||||
result = dict(
|
||||
can_load_key=False,
|
||||
can_parse_key=False,
|
||||
key_is_consistent=None,
|
||||
)
|
||||
if self.content is not None:
|
||||
priv_key_detail = self.content.encode('utf-8')
|
||||
result['can_load_key'] = True
|
||||
else:
|
||||
try:
|
||||
with open(self.path, 'rb') as b_priv_key_fh:
|
||||
priv_key_detail = b_priv_key_fh.read()
|
||||
result['can_load_key'] = True
|
||||
except (IOError, OSError) as exc:
|
||||
self.module.fail_json(msg=to_native(exc), **result)
|
||||
try:
|
||||
self.key = load_privatekey(
|
||||
path=None,
|
||||
content=priv_key_detail,
|
||||
passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase,
|
||||
backend=self.backend
|
||||
)
|
||||
result['can_parse_key'] = True
|
||||
except OpenSSLObjectError as exc:
|
||||
self.module.fail_json(msg=to_native(exc), **result)
|
||||
|
||||
result['public_key'] = self._get_public_key(binary=False)
|
||||
pk = self._get_public_key(binary=True)
|
||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
||||
|
||||
key_type, key_public_data, key_private_data = self._get_key_info()
|
||||
result['type'] = key_type
|
||||
result['public_data'] = key_public_data
|
||||
if self.return_private_key_data:
|
||||
result['private_data'] = key_private_data
|
||||
|
||||
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
|
||||
if result['key_is_consistent'] is False:
|
||||
# Only fail when it is False, to avoid to fail on None (which means "we don't know")
|
||||
result['key_is_consistent'] = False
|
||||
self.module.fail_json(
|
||||
msg="Private key is not consistent! (See "
|
||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)",
|
||||
**result
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class PrivateKeyInfoCryptography(PrivateKeyInfo):
|
||||
"""Validate the supplied private key, using the cryptography backend"""
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyInfoCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
return self.key.public_key().public_bytes(
|
||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
def _get_key_info(self):
|
||||
return _get_cryptography_key_info(self.key)
|
||||
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
|
||||
|
||||
|
||||
class PrivateKeyInfoPyOpenSSL(PrivateKeyInfo):
|
||||
"""validate the supplied private key."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.key
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def bigint_to_int(self, bn):
|
||||
'''Convert OpenSSL BIGINT to Python integer'''
|
||||
if bn == OpenSSL._util.ffi.NULL:
|
||||
return None
|
||||
hexstr = OpenSSL._util.lib.BN_bn2hex(bn)
|
||||
try:
|
||||
return int(OpenSSL._util.ffi.string(hexstr), 16)
|
||||
finally:
|
||||
OpenSSL._util.lib.OPENSSL_free(hexstr)
|
||||
|
||||
def _get_key_info(self):
|
||||
key_public_data = dict()
|
||||
key_private_data = dict()
|
||||
openssl_key_type = self.key.type()
|
||||
try_fallback = True
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
key_type = 'RSA'
|
||||
key_public_data['size'] = self.key.bits()
|
||||
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get modulus and exponents
|
||||
n = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
e = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
d = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
|
||||
key_public_data['modulus'] = self.bigint_to_int(n[0])
|
||||
key_public_data['exponent'] = self.bigint_to_int(e[0])
|
||||
key_private_data['exponent'] = self.bigint_to_int(d[0])
|
||||
# Get factors
|
||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_factors(key, p, q)
|
||||
key_private_data['p'] = self.bigint_to_int(p[0])
|
||||
key_private_data['q'] = self.bigint_to_int(q[0])
|
||||
else:
|
||||
# Get modulus and exponents
|
||||
key_public_data['modulus'] = self.bigint_to_int(key.n)
|
||||
key_public_data['exponent'] = self.bigint_to_int(key.e)
|
||||
key_private_data['exponent'] = self.bigint_to_int(key.d)
|
||||
# Get factors
|
||||
key_private_data['p'] = self.bigint_to_int(key.p)
|
||||
key_private_data['q'] = self.bigint_to_int(key.q)
|
||||
try_fallback = False
|
||||
except AttributeError:
|
||||
# Use fallback if available
|
||||
pass
|
||||
elif crypto.TYPE_DSA == openssl_key_type:
|
||||
key_type = 'DSA'
|
||||
key_public_data['size'] = self.key.bits()
|
||||
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get public parameters (primes and group element)
|
||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
g = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g)
|
||||
key_public_data['p'] = self.bigint_to_int(p[0])
|
||||
key_public_data['q'] = self.bigint_to_int(q[0])
|
||||
key_public_data['g'] = self.bigint_to_int(g[0])
|
||||
# Get public and private key exponents
|
||||
y = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
x = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_key(key, y, x)
|
||||
key_public_data['y'] = self.bigint_to_int(y[0])
|
||||
key_private_data['x'] = self.bigint_to_int(x[0])
|
||||
else:
|
||||
# Get public parameters (primes and group element)
|
||||
key_public_data['p'] = self.bigint_to_int(key.p)
|
||||
key_public_data['q'] = self.bigint_to_int(key.q)
|
||||
key_public_data['g'] = self.bigint_to_int(key.g)
|
||||
# Get public and private key exponents
|
||||
key_public_data['y'] = self.bigint_to_int(key.pub_key)
|
||||
key_private_data['x'] = self.bigint_to_int(key.priv_key)
|
||||
try_fallback = False
|
||||
except AttributeError:
|
||||
# Use fallback if available
|
||||
pass
|
||||
else:
|
||||
# Return 'unknown'
|
||||
key_type = 'unknown ({0})'.format(self.key.type())
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _get_cryptography_key_info(self.key.to_cryptography_key())
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
openssl_key_type = self.key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
try:
|
||||
return self.key.check()
|
||||
except crypto.Error:
|
||||
# OpenSSL error means that key is not consistent
|
||||
return False
|
||||
if crypto.TYPE_DSA == openssl_key_type:
|
||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
||||
if result is not None:
|
||||
return result
|
||||
signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256')
|
||||
# Verify wants a cert (where it can get the public key from)
|
||||
cert = crypto.X509()
|
||||
cert.set_pubkey(self.key)
|
||||
try:
|
||||
crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256')
|
||||
return True
|
||||
except crypto.Error:
|
||||
return False
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str'),
|
||||
content=dict(type='str', no_log=True),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
return_private_key_data=dict(type='bool', default=False),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
@@ -597,49 +229,39 @@ def main():
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
result = dict(
|
||||
can_load_key=False,
|
||||
can_parse_key=False,
|
||||
key_is_consistent=None,
|
||||
)
|
||||
|
||||
if module.params['content'] is not None:
|
||||
data = module.params['content'].encode('utf-8')
|
||||
else:
|
||||
try:
|
||||
with open(module.params['path'], 'rb') as f:
|
||||
data = f.read()
|
||||
except (IOError, OSError) as e:
|
||||
module.fail_json(msg='Error while reading private key file from disk: {0}'.format(e), **result)
|
||||
|
||||
result['can_load_key'] = True
|
||||
|
||||
backend, module_backend = select_backend(
|
||||
module,
|
||||
module.params['select_crypto_backend'],
|
||||
data,
|
||||
passphrase=module.params['passphrase'],
|
||||
return_private_key_data=module.params['return_private_key_data'])
|
||||
|
||||
try:
|
||||
if module.params['path'] is not None:
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detect what backend we can use
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# If cryptography is available we'll use it
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Fail if no backend has been found
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
privatekey = PrivateKeyInfoPyOpenSSL(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
privatekey = PrivateKeyInfoCryptography(module)
|
||||
|
||||
result = privatekey.get_info()
|
||||
result.update(module_backend.get_info())
|
||||
module.exit_json(**result)
|
||||
except PrivateKeyParseError as exc:
|
||||
result.update(exc.result)
|
||||
module.fail_json(msg=exc.error_message, **result)
|
||||
except PrivateKeyConsistencyError as exc:
|
||||
result.update(exc.result)
|
||||
module.fail_json(msg=exc.error_message, **result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
117
plugins/modules/openssl_privatekey_pipe.py
Normal file
117
plugins/modules/openssl_privatekey_pipe.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: openssl_privatekey_pipe
|
||||
short_description: Generate OpenSSL private keys without disk access
|
||||
version_added: 1.3.0
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL private keys without disk access.
|
||||
- This allows to read and write keys to vaults without having to write intermediate versions to disk.
|
||||
- Make sure to not write the result of this module into logs or to the console, as it contains private key data! Use the I(no_log) task option to be sure.
|
||||
- Note that this module is implemented as an L(action plugin,https://docs.ansible.com/ansible/latest/plugins/action.html)
|
||||
and will always be executed on the controller.
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
- Felix Fontein (@felixfontein)
|
||||
options:
|
||||
content:
|
||||
description:
|
||||
- The current private key data.
|
||||
- Needed for idempotency. If not provided, the module will always return a change, and all idempotence-related
|
||||
options are ignored.
|
||||
type: str
|
||||
content_base64:
|
||||
description:
|
||||
- Set to C(true) if the content is base64 encoded.
|
||||
type: bool
|
||||
default: false
|
||||
return_current_key:
|
||||
description:
|
||||
- Set to C(true) to return the current private key when the module did not generate a new one.
|
||||
- Note that in case of check mode, when this option is not set to C(true), the module always returns the
|
||||
current key (if it was provided) and Ansible will replace it by C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER).
|
||||
type: bool
|
||||
default: false
|
||||
extends_documentation_fragment:
|
||||
- community.crypto.module_privatekey
|
||||
seealso:
|
||||
- module: community.crypto.openssl_privatekey
|
||||
- module: community.crypto.openssl_privatekey_info
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
|
||||
community.crypto.openssl_privatekey_pipe:
|
||||
register: output
|
||||
no_log: true # make sure that private key data is not accidentally revealed in logs!
|
||||
- name: Show generated key
|
||||
debug:
|
||||
msg: "{{ output.privatekey }}"
|
||||
# DO NOT OUTPUT KEY MATERIAL TO CONSOLE OR LOGS IN PRODUCTION!
|
||||
|
||||
- block:
|
||||
- name: Update sops-encrypted key with the community.sops collection
|
||||
community.crypto.openssl_privatekey_pipe:
|
||||
content: "{{ lookup('community.sops.sops', 'private_key.pem.sops') }}"
|
||||
size: 2048
|
||||
register: output
|
||||
no_log: true # make sure that private key data is not accidentally revealed in logs!
|
||||
|
||||
- name: Update encrypted key when openssl_privatekey_pipe reported a change
|
||||
community.sops.sops_encrypt:
|
||||
path: private_key.pem.sops
|
||||
content_text: "{{ output.privatekey }}"
|
||||
when: output is changed
|
||||
always:
|
||||
- name: Make sure that output (which contains the private key) is overwritten
|
||||
set_fact:
|
||||
output: ''
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
size:
|
||||
description: Size (in bits) of the TLS/SSL private key.
|
||||
returned: changed or success
|
||||
type: int
|
||||
sample: 4096
|
||||
type:
|
||||
description: Algorithm used to generate the TLS/SSL private key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: RSA
|
||||
curve:
|
||||
description: Elliptic curve used to generate the TLS/SSL private key.
|
||||
returned: changed or success, and I(type) is C(ECC)
|
||||
type: str
|
||||
sample: secp256r1
|
||||
fingerprint:
|
||||
description:
|
||||
- The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
|
||||
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
|
||||
returned: changed or success
|
||||
type: dict
|
||||
sample:
|
||||
md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
|
||||
sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
|
||||
sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
|
||||
sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
|
||||
sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
|
||||
sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
|
||||
privatekey:
|
||||
description:
|
||||
- The generated private key's content.
|
||||
- Please note that if the result is not changed, the current private key will only be returned
|
||||
if the I(return_current_key) option is set to C(true).
|
||||
- Will be Base64-encoded if the key is in raw format.
|
||||
returned: changed, or I(return_current_key) is C(true)
|
||||
type: str
|
||||
'''
|
||||
@@ -13,8 +13,9 @@ DOCUMENTATION = r'''
|
||||
module: openssl_publickey
|
||||
short_description: Generate an OpenSSL public key from its private key.
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL public keys from their private keys.
|
||||
- Keys are generated in PEM or OpenSSH format.
|
||||
- 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.
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. When I(format) is C(OpenSSH),
|
||||
@@ -92,10 +93,13 @@ extends_documentation_fragment:
|
||||
- files
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.x509_certificate_pipe
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_csr_pipe
|
||||
- module: community.crypto.openssl_dhparam
|
||||
- module: community.crypto.openssl_pkcs12
|
||||
- module: community.crypto.openssl_privatekey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -179,10 +183,10 @@ publickey:
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
@@ -200,6 +204,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
get_fingerprint,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
PublicKeyParseError,
|
||||
get_publickey_info,
|
||||
)
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '16.0.0'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
|
||||
@@ -209,7 +218,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
@@ -241,6 +250,7 @@ class PublicKey(OpenSSLObject):
|
||||
module.params['force'],
|
||||
module.check_mode
|
||||
)
|
||||
self.module = module
|
||||
self.format = module.params['format']
|
||||
self.privatekey_path = module.params['privatekey_path']
|
||||
self.privatekey_content = module.params['privatekey_content']
|
||||
@@ -256,6 +266,23 @@ class PublicKey(OpenSSLObject):
|
||||
self.backup = module.params['backup']
|
||||
self.backup_file = None
|
||||
|
||||
self.diff_before = self._get_info(None)
|
||||
self.diff_after = self._get_info(None)
|
||||
|
||||
def _get_info(self, data):
|
||||
if data is None:
|
||||
return dict()
|
||||
result = dict(can_parse_key=False)
|
||||
try:
|
||||
result.update(get_publickey_info(
|
||||
self.module, self.backend, content=data, prefer_one_fingerprint=True))
|
||||
result['can_parse_key'] = True
|
||||
except PublicKeyParseError as exc:
|
||||
result.update(exc.result)
|
||||
except Exception as exc:
|
||||
pass
|
||||
return result
|
||||
|
||||
def _create_publickey(self, module):
|
||||
self.privatekey = load_privatekey(
|
||||
path=self.privatekey_path,
|
||||
@@ -291,6 +318,7 @@ class PublicKey(OpenSSLObject):
|
||||
if not self.check(module, perms_required=False) or self.force:
|
||||
try:
|
||||
publickey_content = self._create_publickey(module)
|
||||
self.diff_after = self._get_info(publickey_content)
|
||||
if self.return_content:
|
||||
self.publickey_bytes = publickey_content
|
||||
|
||||
@@ -311,7 +339,9 @@ class PublicKey(OpenSSLObject):
|
||||
backend=self.backend,
|
||||
)
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||
self.changed = True
|
||||
elif module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
|
||||
def check(self, module, perms_required=True):
|
||||
@@ -326,6 +356,7 @@ class PublicKey(OpenSSLObject):
|
||||
try:
|
||||
with open(self.path, 'rb') as public_key_fh:
|
||||
publickey_content = public_key_fh.read()
|
||||
self.diff_before = self.diff_after = self._get_info(publickey_content)
|
||||
if self.return_content:
|
||||
self.publickey_bytes = publickey_content
|
||||
if self.backend == 'cryptography':
|
||||
@@ -384,6 +415,11 @@ class PublicKey(OpenSSLObject):
|
||||
self.publickey_bytes = load_file_if_exists(self.path, ignore_errors=True)
|
||||
result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None
|
||||
|
||||
result['diff'] = dict(
|
||||
before=self.diff_before,
|
||||
after=self.diff_after,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -395,7 +431,7 @@ def main():
|
||||
force=dict(type='bool', default=False),
|
||||
path=dict(type='path', required=True),
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
backup=dict(type='bool', default=False),
|
||||
|
||||
219
plugins/modules/openssl_publickey_info.py
Normal file
219
plugins/modules/openssl_publickey_info.py
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: openssl_publickey_info
|
||||
short_description: Provide information for OpenSSL public keys
|
||||
description:
|
||||
- This module allows one to query information on OpenSSL public keys.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
||||
and will be removed in community.crypto 2.0.0.
|
||||
version_added: 1.7.0
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
|
||||
author:
|
||||
- Felix Fontein (@felixfontein)
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Remote absolute path where the public key file is loaded from.
|
||||
type: path
|
||||
content:
|
||||
description:
|
||||
- Content of the public key file.
|
||||
- Either I(path) or I(content) must be specified, but not both.
|
||||
type: str
|
||||
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.openssl_publickey
|
||||
- module: community.crypto.openssl_privatekey_info
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
|
||||
community.crypto.openssl_privatekey:
|
||||
path: /etc/ssl/private/ansible.com.pem
|
||||
|
||||
- name: Create public key from private key
|
||||
community.crypto.openssl_publickey:
|
||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
||||
path: /etc/ssl/ansible.com.pub
|
||||
|
||||
- name: Get information on public key
|
||||
community.crypto.openssl_publickey_info:
|
||||
path: /etc/ssl/ansible.com.pub
|
||||
register: result
|
||||
|
||||
- name: Dump information
|
||||
ansible.builtin.debug:
|
||||
var: result
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
fingerprints:
|
||||
description:
|
||||
- Fingerprints of public key.
|
||||
- For every hash algorithm available, the fingerprint is computed.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
||||
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
||||
type:
|
||||
description:
|
||||
- The key's type.
|
||||
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
||||
- Will start with C(unknown) if the key type cannot be determined.
|
||||
returned: success
|
||||
type: str
|
||||
sample: RSA
|
||||
public_data:
|
||||
description:
|
||||
- Public key data. Depends on key type.
|
||||
returned: success
|
||||
type: dict
|
||||
contains:
|
||||
size:
|
||||
description:
|
||||
- Bit size of modulus (RSA) or prime number (DSA).
|
||||
type: int
|
||||
returned: When C(type=RSA) or C(type=DSA)
|
||||
modulus:
|
||||
description:
|
||||
- The RSA key's modulus.
|
||||
type: int
|
||||
returned: When C(type=RSA)
|
||||
exponent:
|
||||
description:
|
||||
- The RSA key's public exponent.
|
||||
type: int
|
||||
returned: When C(type=RSA)
|
||||
p:
|
||||
description:
|
||||
- The C(p) value for DSA.
|
||||
- This is the prime modulus upon which arithmetic takes place.
|
||||
type: int
|
||||
returned: When C(type=DSA)
|
||||
q:
|
||||
description:
|
||||
- The C(q) value for DSA.
|
||||
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||
multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(type=DSA)
|
||||
g:
|
||||
description:
|
||||
- The C(g) value for DSA.
|
||||
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(type=DSA)
|
||||
curve:
|
||||
description:
|
||||
- The curve's name for ECC.
|
||||
type: str
|
||||
returned: When C(type=ECC)
|
||||
exponent_size:
|
||||
description:
|
||||
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||
type: int
|
||||
returned: When C(type=ECC)
|
||||
x:
|
||||
description:
|
||||
- The C(x) coordinate for the public point on the elliptic curve.
|
||||
type: int
|
||||
returned: When C(type=ECC)
|
||||
y:
|
||||
description:
|
||||
- For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||
- For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||
type: int
|
||||
returned: When C(type=DSA) or C(type=ECC)
|
||||
'''
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
PublicKeyParseError,
|
||||
select_backend,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str', no_log=True),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
),
|
||||
required_one_of=(
|
||||
['path', 'content'],
|
||||
),
|
||||
mutually_exclusive=(
|
||||
['path', 'content'],
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
result = dict(
|
||||
can_load_key=False,
|
||||
can_parse_key=False,
|
||||
key_is_consistent=None,
|
||||
)
|
||||
|
||||
if module.params['content'] is not None:
|
||||
data = module.params['content'].encode('utf-8')
|
||||
else:
|
||||
try:
|
||||
with open(module.params['path'], 'rb') as f:
|
||||
data = f.read()
|
||||
except (IOError, OSError) as e:
|
||||
module.fail_json(msg='Error while reading public key file from disk: {0}'.format(e), **result)
|
||||
|
||||
backend, module_backend = select_backend(
|
||||
module,
|
||||
module.params['select_crypto_backend'],
|
||||
data)
|
||||
|
||||
try:
|
||||
result.update(module_backend.get_info())
|
||||
module.exit_json(**result)
|
||||
except PublicKeyParseError as exc:
|
||||
result.update(exc.result)
|
||||
module.fail_json(msg=exc.error_message, **result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
323
plugins/modules/openssl_signature.py
Normal file
323
plugins/modules/openssl_signature.py
Normal file
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: openssl_signature
|
||||
version_added: 1.1.0
|
||||
short_description: Sign data with openssl
|
||||
description:
|
||||
- This module allows one to sign data using a private key.
|
||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
|
||||
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
requirements:
|
||||
- Either cryptography >= 1.4 (some key types require newer versions)
|
||||
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
|
||||
author:
|
||||
- Patrick Pichler (@aveexy)
|
||||
- Markus Teufelberger (@MarkusTeufelberger)
|
||||
options:
|
||||
privatekey_path:
|
||||
description:
|
||||
- The path to the private key to use when signing.
|
||||
- Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
|
||||
type: path
|
||||
privatekey_content:
|
||||
description:
|
||||
- The content of the private key to use when signing the certificate signing request.
|
||||
- Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
|
||||
type: str
|
||||
privatekey_passphrase:
|
||||
description:
|
||||
- The passphrase for the private key.
|
||||
- This is required if the private key is password protected.
|
||||
type: str
|
||||
path:
|
||||
description:
|
||||
- The file to sign.
|
||||
- This file will only be read and not modified.
|
||||
type: path
|
||||
required: true
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
notes:
|
||||
- |
|
||||
When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
|
||||
RSA keys: C(cryptography) >= 1.4
|
||||
DSA and ECDSA keys: C(cryptography) >= 1.5
|
||||
ed448 and ed25519 keys: C(cryptography) >= 2.6
|
||||
seealso:
|
||||
- module: community.crypto.openssl_signature_info
|
||||
- module: community.crypto.openssl_privatekey
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Sign example file
|
||||
community.crypto.openssl_signature:
|
||||
privatekey_path: private.key
|
||||
path: /tmp/example_file
|
||||
register: sig
|
||||
|
||||
- name: Verify signature of example file
|
||||
community.crypto.openssl_signature_info:
|
||||
certificate_path: cert.pem
|
||||
path: /tmp/example_file
|
||||
signature: "{{ sig.signature }}"
|
||||
register: verify
|
||||
|
||||
- name: Make sure the signature is valid
|
||||
assert:
|
||||
that:
|
||||
- verify.valid
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
signature:
|
||||
description: Base64 encoded signature.
|
||||
returned: success
|
||||
type: str
|
||||
'''
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import base64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.hazmat.primitives.asymmetric.padding
|
||||
import cryptography.hazmat.primitives.hashes
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN,
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN,
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_privatekey,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
|
||||
|
||||
class SignatureBase(OpenSSLObject):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignatureBase, self).__init__(
|
||||
path=module.params['path'],
|
||||
state='present',
|
||||
force=False,
|
||||
check_mode=module.check_mode
|
||||
)
|
||||
|
||||
self.backend = backend
|
||||
|
||||
self.privatekey_path = module.params['privatekey_path']
|
||||
self.privatekey_content = module.params['privatekey_content']
|
||||
if self.privatekey_content is not None:
|
||||
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||
|
||||
def generate(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class SignaturePyOpenSSL(SignatureBase):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignaturePyOpenSSL, self).__init__(module, backend)
|
||||
|
||||
def run(self):
|
||||
|
||||
result = dict()
|
||||
|
||||
try:
|
||||
with open(self.path, "rb") as f:
|
||||
_in = f.read()
|
||||
|
||||
private_key = load_privatekey(
|
||||
path=self.privatekey_path,
|
||||
content=self.privatekey_content,
|
||||
passphrase=self.privatekey_passphrase,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
signature = OpenSSL.crypto.sign(private_key, _in, "sha256")
|
||||
result['signature'] = base64.b64encode(signature)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError(e)
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class SignatureCryptography(SignatureBase):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignatureCryptography, self).__init__(module, backend)
|
||||
|
||||
def run(self):
|
||||
_padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
|
||||
_hash = cryptography.hazmat.primitives.hashes.SHA256()
|
||||
|
||||
result = dict()
|
||||
|
||||
try:
|
||||
with open(self.path, "rb") as f:
|
||||
_in = f.read()
|
||||
|
||||
private_key = load_privatekey(
|
||||
path=self.privatekey_path,
|
||||
content=self.privatekey_content,
|
||||
passphrase=self.privatekey_passphrase,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
signature = None
|
||||
|
||||
if CRYPTOGRAPHY_HAS_DSA_SIGN:
|
||||
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
signature = private_key.sign(_in, _hash)
|
||||
|
||||
if CRYPTOGRAPHY_HAS_EC_SIGN:
|
||||
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
signature = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
|
||||
|
||||
if CRYPTOGRAPHY_HAS_ED25519_SIGN:
|
||||
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
signature = private_key.sign(_in)
|
||||
|
||||
if CRYPTOGRAPHY_HAS_ED448_SIGN:
|
||||
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
signature = private_key.sign(_in)
|
||||
|
||||
if CRYPTOGRAPHY_HAS_RSA_SIGN:
|
||||
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
signature = private_key.sign(_in, _padding, _hash)
|
||||
|
||||
if signature is None:
|
||||
self.module.fail_json(
|
||||
msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
|
||||
)
|
||||
|
||||
result['signature'] = base64.b64encode(signature)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError(e)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
path=dict(type='path', required=True),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
),
|
||||
mutually_exclusive=(
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
),
|
||||
required_one_of=(
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not os.path.isfile(module.params['path']):
|
||||
module.fail_json(
|
||||
name=module.params['path'],
|
||||
msg='The file {0} does not exist'.format(module.params['path'])
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
try:
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
_sign = SignaturePyOpenSSL(module, backend)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
_sign = SignatureCryptography(module, backend)
|
||||
|
||||
result = _sign.run()
|
||||
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
356
plugins/modules/openssl_signature_info.py
Normal file
356
plugins/modules/openssl_signature_info.py
Normal file
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: openssl_signature_info
|
||||
version_added: 1.1.0
|
||||
short_description: Verify signatures with openssl
|
||||
description:
|
||||
- This module allows one to verify a signature for a file by a certificate.
|
||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
|
||||
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
requirements:
|
||||
- Either cryptography >= 1.4 (some key types require newer versions)
|
||||
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
|
||||
author:
|
||||
- Patrick Pichler (@aveexy)
|
||||
- Markus Teufelberger (@MarkusTeufelberger)
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The signed file to verify.
|
||||
- This file will only be read and not modified.
|
||||
type: path
|
||||
required: true
|
||||
certificate_path:
|
||||
description:
|
||||
- The path to the certificate used to verify the signature.
|
||||
- Either I(certificate_path) or I(certificate_content) must be specified, but not both.
|
||||
type: path
|
||||
certificate_content:
|
||||
description:
|
||||
- The content of the certificate used to verify the signature.
|
||||
- Either I(certificate_path) or I(certificate_content) must be specified, but not both.
|
||||
type: str
|
||||
signature:
|
||||
description: Base64 encoded signature.
|
||||
type: str
|
||||
required: true
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
notes:
|
||||
- |
|
||||
When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
|
||||
RSA keys: C(cryptography) >= 1.4
|
||||
DSA and ECDSA keys: C(cryptography) >= 1.5
|
||||
ed448 and ed25519 keys: C(cryptography) >= 2.6
|
||||
- Supports C(check_mode).
|
||||
seealso:
|
||||
- module: community.crypto.openssl_signature
|
||||
- module: community.crypto.x509_certificate
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Sign example file
|
||||
community.crypto.openssl_signature:
|
||||
privatekey_path: private.key
|
||||
path: /tmp/example_file
|
||||
register: sig
|
||||
|
||||
- name: Verify signature of example file
|
||||
community.crypto.openssl_signature_info:
|
||||
certificate_path: cert.pem
|
||||
path: /tmp/example_file
|
||||
signature: "{{ sig.signature }}"
|
||||
register: verify
|
||||
|
||||
- name: Make sure the signature is valid
|
||||
assert:
|
||||
that:
|
||||
- verify.valid
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
valid:
|
||||
description: C(true) means the signature was valid for the given file, C(false) means it was not.
|
||||
returned: success
|
||||
type: bool
|
||||
'''
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import base64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.hazmat.primitives.asymmetric.padding
|
||||
import cryptography.hazmat.primitives.hashes
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_DSA_SIGN,
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN,
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_certificate,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
|
||||
|
||||
class SignatureInfoBase(OpenSSLObject):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignatureInfoBase, self).__init__(
|
||||
path=module.params['path'],
|
||||
state='present',
|
||||
force=False,
|
||||
check_mode=module.check_mode
|
||||
)
|
||||
|
||||
self.backend = backend
|
||||
|
||||
self.signature = module.params['signature']
|
||||
self.certificate_path = module.params['certificate_path']
|
||||
self.certificate_content = module.params['certificate_content']
|
||||
if self.certificate_content is not None:
|
||||
self.certificate_content = self.certificate_content.encode('utf-8')
|
||||
|
||||
def generate(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class SignatureInfoPyOpenSSL(SignatureInfoBase):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignatureInfoPyOpenSSL, self).__init__(module, backend)
|
||||
|
||||
def run(self):
|
||||
|
||||
result = dict()
|
||||
|
||||
try:
|
||||
with open(self.path, "rb") as f:
|
||||
_in = f.read()
|
||||
|
||||
_signature = base64.b64decode(self.signature)
|
||||
certificate = load_certificate(
|
||||
path=self.certificate_path,
|
||||
content=self.certificate_content,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
try:
|
||||
OpenSSL.crypto.verify(certificate, _signature, _in, 'sha256')
|
||||
result['valid'] = True
|
||||
except Exception:
|
||||
result['valid'] = False
|
||||
return result
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError(e)
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class SignatureInfoCryptography(SignatureInfoBase):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignatureInfoCryptography, self).__init__(module, backend)
|
||||
|
||||
def run(self):
|
||||
_padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
|
||||
_hash = cryptography.hazmat.primitives.hashes.SHA256()
|
||||
|
||||
result = dict()
|
||||
|
||||
try:
|
||||
with open(self.path, "rb") as f:
|
||||
_in = f.read()
|
||||
|
||||
_signature = base64.b64decode(self.signature)
|
||||
certificate = load_certificate(
|
||||
path=self.certificate_path,
|
||||
content=self.certificate_content,
|
||||
backend=self.backend,
|
||||
)
|
||||
public_key = certificate.public_key()
|
||||
verified = False
|
||||
valid = False
|
||||
|
||||
if CRYPTOGRAPHY_HAS_DSA_SIGN:
|
||||
try:
|
||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
|
||||
public_key.verify(_signature, _in, _hash)
|
||||
verified = True
|
||||
valid = True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
verified = True
|
||||
valid = False
|
||||
|
||||
if CRYPTOGRAPHY_HAS_EC_SIGN:
|
||||
try:
|
||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
||||
public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
|
||||
verified = True
|
||||
valid = True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
verified = True
|
||||
valid = False
|
||||
|
||||
if CRYPTOGRAPHY_HAS_ED25519_SIGN:
|
||||
try:
|
||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
|
||||
public_key.verify(_signature, _in)
|
||||
verified = True
|
||||
valid = True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
verified = True
|
||||
valid = False
|
||||
|
||||
if CRYPTOGRAPHY_HAS_ED448_SIGN:
|
||||
try:
|
||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
|
||||
public_key.verify(_signature, _in)
|
||||
verified = True
|
||||
valid = True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
verified = True
|
||||
valid = False
|
||||
|
||||
if CRYPTOGRAPHY_HAS_RSA_SIGN:
|
||||
try:
|
||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
||||
public_key.verify(_signature, _in, _padding, _hash)
|
||||
verified = True
|
||||
valid = True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
verified = True
|
||||
valid = False
|
||||
|
||||
if not verified:
|
||||
self.module.fail_json(
|
||||
msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
|
||||
)
|
||||
result['valid'] = valid
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError(e)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
certificate_path=dict(type='path'),
|
||||
certificate_content=dict(type='str'),
|
||||
path=dict(type='path', required=True),
|
||||
signature=dict(type='str', required=True),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
),
|
||||
mutually_exclusive=(
|
||||
['certificate_path', 'certificate_content'],
|
||||
),
|
||||
required_one_of=(
|
||||
['certificate_path', 'certificate_content'],
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not os.path.isfile(module.params['path']):
|
||||
module.fail_json(
|
||||
name=module.params['path'],
|
||||
msg='The file {0} does not exist'.format(module.params['path'])
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
try:
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
_sign = SignatureInfoPyOpenSSL(module, backend)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
_sign = SignatureInfoCryptography(module, backend)
|
||||
|
||||
result = _sign.run()
|
||||
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@ options:
|
||||
- Time can be specified either as relative time or as absolute timestamp.
|
||||
- Time will always be interpreted as UTC.
|
||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (i.e. pattern C(YYYYMMDDHHMMSSZ)).
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (in other words, pattern C(YYYYMMDDHHMMSSZ)).
|
||||
Note that all timestamps will be treated as being in UTC.
|
||||
type: dict
|
||||
select_crypto_backend:
|
||||
@@ -70,10 +70,12 @@ options:
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
notes:
|
||||
- All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
They are all in UTC.
|
||||
- Supports C(check_mode).
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.x509_certificate_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -93,7 +95,7 @@ EXAMPLES = r'''
|
||||
register: result
|
||||
|
||||
- name: Dump information
|
||||
debug:
|
||||
ansible.builtin.debug:
|
||||
var: result
|
||||
|
||||
|
||||
@@ -119,7 +121,7 @@ EXAMPLES = r'''
|
||||
|
||||
RETURN = r'''
|
||||
expired:
|
||||
description: Whether the certificate is expired (i.e. C(notAfter) is in the past)
|
||||
description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
|
||||
returned: success
|
||||
type: bool
|
||||
basic_constraints:
|
||||
@@ -127,7 +129,7 @@ basic_constraints:
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[CA:TRUE, pathlen:1]"
|
||||
sample: ["CA:TRUE", "pathlen:1"]
|
||||
basic_constraints_critical:
|
||||
description: Whether the C(basic_constraints) extension is critical.
|
||||
returned: success
|
||||
@@ -137,13 +139,13 @@ extended_key_usage:
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[Biometric Info, DVCS, Time Stamping]"
|
||||
sample: [Biometric Info, DVCS, Time Stamping]
|
||||
extended_key_usage_critical:
|
||||
description: Whether the C(extended_key_usage) extension is critical.
|
||||
returned: success
|
||||
type: bool
|
||||
extensions_by_oid:
|
||||
description: Returns a dictionary for every extension OID
|
||||
description: Returns a dictionary for every extension OID.
|
||||
returned: success
|
||||
type: dict
|
||||
contains:
|
||||
@@ -152,16 +154,16 @@ extensions_by_oid:
|
||||
returned: success
|
||||
type: bool
|
||||
value:
|
||||
description: The Base64 encoded value (in DER format) of the extension
|
||||
description: The Base64 encoded value (in DER format) of the extension.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "MAMCAQU="
|
||||
sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}'
|
||||
sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}
|
||||
key_usage:
|
||||
description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "[Key Agreement, Data Encipherment]"
|
||||
sample: [Key Agreement, Data Encipherment]
|
||||
key_usage_critical:
|
||||
description: Whether the C(key_usage) extension is critical.
|
||||
returned: success
|
||||
@@ -171,7 +173,7 @@ subject_alt_name:
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
||||
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
|
||||
subject_alt_name_critical:
|
||||
description: Whether the C(subject_alt_name) extension is critical.
|
||||
returned: success
|
||||
@@ -190,41 +192,112 @@ issuer:
|
||||
- Note that for repeated values, only the last one will be returned.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
||||
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
|
||||
issuer_ordered:
|
||||
description: The certificate's issuer as an ordered list of tuples.
|
||||
returned: success
|
||||
type: list
|
||||
elements: list
|
||||
sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
|
||||
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
|
||||
subject:
|
||||
description:
|
||||
- The certificate's subject as a dictionary.
|
||||
- Note that for repeated values, only the last one will be returned.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}'
|
||||
sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
|
||||
subject_ordered:
|
||||
description: The certificate's subject as an ordered list of tuples.
|
||||
returned: success
|
||||
type: list
|
||||
elements: list
|
||||
sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]'
|
||||
sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
|
||||
not_after:
|
||||
description: C(notAfter) date as ASN.1 TIME
|
||||
description: C(notAfter) date as ASN.1 TIME.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 20190413202428Z
|
||||
sample: '20190413202428Z'
|
||||
not_before:
|
||||
description: C(notBefore) date as ASN.1 TIME
|
||||
description: C(notBefore) date as ASN.1 TIME.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 20190331202428Z
|
||||
sample: '20190331202428Z'
|
||||
public_key:
|
||||
description: Certificate's public key in PEM format
|
||||
description: Certificate's public key in PEM format.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||
public_key_type:
|
||||
description:
|
||||
- The certificate's public key's type.
|
||||
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
||||
- Will start with C(unknown) if the key type cannot be determined.
|
||||
returned: success
|
||||
type: str
|
||||
version_added: 1.7.0
|
||||
sample: RSA
|
||||
public_key_data:
|
||||
description:
|
||||
- Public key data. Depends on the public key's type.
|
||||
returned: success
|
||||
type: dict
|
||||
version_added: 1.7.0
|
||||
contains:
|
||||
size:
|
||||
description:
|
||||
- Bit size of modulus (RSA) or prime number (DSA).
|
||||
type: int
|
||||
returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
|
||||
modulus:
|
||||
description:
|
||||
- The RSA key's modulus.
|
||||
type: int
|
||||
returned: When C(public_key_type=RSA)
|
||||
exponent:
|
||||
description:
|
||||
- The RSA key's public exponent.
|
||||
type: int
|
||||
returned: When C(public_key_type=RSA)
|
||||
p:
|
||||
description:
|
||||
- The C(p) value for DSA.
|
||||
- This is the prime modulus upon which arithmetic takes place.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA)
|
||||
q:
|
||||
description:
|
||||
- The C(q) value for DSA.
|
||||
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||
multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA)
|
||||
g:
|
||||
description:
|
||||
- The C(g) value for DSA.
|
||||
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA)
|
||||
curve:
|
||||
description:
|
||||
- The curve's name for ECC.
|
||||
type: str
|
||||
returned: When C(public_key_type=ECC)
|
||||
exponent_size:
|
||||
description:
|
||||
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||
type: int
|
||||
returned: When C(public_key_type=ECC)
|
||||
x:
|
||||
description:
|
||||
- The C(x) coordinate for the public point on the elliptic curve.
|
||||
type: int
|
||||
returned: When C(public_key_type=ECC)
|
||||
y:
|
||||
description:
|
||||
- For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||
- For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||
type: int
|
||||
returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
|
||||
public_key_fingerprints:
|
||||
description:
|
||||
- Fingerprints of certificate's public key.
|
||||
@@ -233,6 +306,15 @@ public_key_fingerprints:
|
||||
type: dict
|
||||
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
||||
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
||||
fingerprints:
|
||||
description:
|
||||
- Fingerprints of the DER-encoded form of the whole certificate.
|
||||
- For every hash algorithm available, the fingerprint is computed.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
||||
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
||||
version_added: 1.2.0
|
||||
signature_algorithm:
|
||||
description: The signature algorithm used to sign the certificate.
|
||||
returned: success
|
||||
@@ -277,14 +359,14 @@ authority_cert_issuer:
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
||||
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
|
||||
authority_cert_serial_number:
|
||||
description:
|
||||
- The certificate's authority cert serial number.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
type: int
|
||||
sample: '12345'
|
||||
sample: 12345
|
||||
ocsp_uri:
|
||||
description: The OCSP responder URI, if included in the certificate. Will be
|
||||
C(none) if no OCSP responder URI is included.
|
||||
@@ -293,517 +375,22 @@ ocsp_uri:
|
||||
'''
|
||||
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
get_relative_time_option,
|
||||
load_certificate,
|
||||
get_fingerprint_of_bytes,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_decode_name,
|
||||
cryptography_get_extensions_from_cert,
|
||||
cryptography_oid_to_name,
|
||||
cryptography_serial_number_of_cert,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
|
||||
select_backend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_get_extensions_from_cert,
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
|
||||
class CertificateInfo(OpenSSLObject):
|
||||
def __init__(self, module, backend):
|
||||
super(CertificateInfo, self).__init__(
|
||||
module.params['path'] or '',
|
||||
'present',
|
||||
False,
|
||||
module.check_mode,
|
||||
)
|
||||
self.backend = backend
|
||||
self.module = module
|
||||
self.content = module.params['content']
|
||||
if self.content is not None:
|
||||
self.content = self.content.encode('utf-8')
|
||||
|
||||
self.valid_at = module.params['valid_at']
|
||||
if self.valid_at:
|
||||
for k, v in self.valid_at.items():
|
||||
if not isinstance(v, string_types):
|
||||
self.module.fail_json(
|
||||
msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
|
||||
)
|
||||
self.valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k))
|
||||
|
||||
def generate(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_signature_algorithm(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_ordered(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_issuer_ordered(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_version(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_basic_constraints(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ocsp_must_staple(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_not_before(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_not_after(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_subject_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_authority_key_identifier(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_serial_number(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_all_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ocsp_uri(self):
|
||||
pass
|
||||
|
||||
def get_info(self):
|
||||
result = dict()
|
||||
self.cert = load_certificate(self.path, content=self.content, backend=self.backend)
|
||||
|
||||
result['signature_algorithm'] = self._get_signature_algorithm()
|
||||
subject = self._get_subject_ordered()
|
||||
issuer = self._get_issuer_ordered()
|
||||
result['subject'] = dict()
|
||||
for k, v in subject:
|
||||
result['subject'][k] = v
|
||||
result['subject_ordered'] = subject
|
||||
result['issuer'] = dict()
|
||||
for k, v in issuer:
|
||||
result['issuer'][k] = v
|
||||
result['issuer_ordered'] = issuer
|
||||
result['version'] = self._get_version()
|
||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
||||
|
||||
not_before = self._get_not_before()
|
||||
not_after = self._get_not_after()
|
||||
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
|
||||
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
|
||||
result['expired'] = not_after < datetime.datetime.utcnow()
|
||||
|
||||
result['valid_at'] = dict()
|
||||
if self.valid_at:
|
||||
for k, v in self.valid_at.items():
|
||||
result['valid_at'][k] = not_before <= v <= not_after
|
||||
|
||||
result['public_key'] = self._get_public_key(binary=False)
|
||||
pk = self._get_public_key(binary=True)
|
||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
|
||||
result['serial_number'] = self._get_serial_number()
|
||||
result['extensions_by_oid'] = self._get_all_extensions()
|
||||
result['ocsp_uri'] = self._get_ocsp_uri()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class CertificateInfoCryptography(CertificateInfo):
|
||||
"""Validate the supplied cert, using the cryptography backend"""
|
||||
def __init__(self, module):
|
||||
super(CertificateInfoCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def _get_signature_algorithm(self):
|
||||
return cryptography_oid_to_name(self.cert.signature_algorithm_oid)
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
result = []
|
||||
for attribute in self.cert.subject:
|
||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
return result
|
||||
|
||||
def _get_issuer_ordered(self):
|
||||
result = []
|
||||
for attribute in self.cert.issuer:
|
||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
return result
|
||||
|
||||
def _get_version(self):
|
||||
if self.cert.version == x509.Version.v1:
|
||||
return 1
|
||||
if self.cert.version == x509.Version.v3:
|
||||
return 3
|
||||
return "unknown"
|
||||
|
||||
def _get_key_usage(self):
|
||||
try:
|
||||
current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage)
|
||||
current_key_usage = current_key_ext.value
|
||||
key_usage = dict(
|
||||
digital_signature=current_key_usage.digital_signature,
|
||||
content_commitment=current_key_usage.content_commitment,
|
||||
key_encipherment=current_key_usage.key_encipherment,
|
||||
data_encipherment=current_key_usage.data_encipherment,
|
||||
key_agreement=current_key_usage.key_agreement,
|
||||
key_cert_sign=current_key_usage.key_cert_sign,
|
||||
crl_sign=current_key_usage.crl_sign,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
)
|
||||
if key_usage['key_agreement']:
|
||||
key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usage_names = dict(
|
||||
digital_signature='Digital Signature',
|
||||
content_commitment='Non Repudiation',
|
||||
key_encipherment='Key Encipherment',
|
||||
data_encipherment='Data Encipherment',
|
||||
key_agreement='Key Agreement',
|
||||
key_cert_sign='Certificate Sign',
|
||||
crl_sign='CRL Sign',
|
||||
encipher_only='Encipher Only',
|
||||
decipher_only='Decipher Only',
|
||||
)
|
||||
return sorted([
|
||||
key_usage_names[name] for name, value in key_usage.items() if value
|
||||
]), current_key_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
||||
return sorted([
|
||||
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
||||
]), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
try:
|
||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints)
|
||||
result = []
|
||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
||||
if ext_keyusage_ext.value.path_length is not None:
|
||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
||||
return sorted(result), ext_keyusage_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
try:
|
||||
try:
|
||||
# This only works with cryptography >= 2.1
|
||||
tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature)
|
||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||
except AttributeError as dummy:
|
||||
# Fallback for cryptography < 2.1
|
||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||
tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid)
|
||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
||||
return value, tlsfeature_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
try:
|
||||
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
||||
return result, san_ext.critical
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, False
|
||||
|
||||
def _get_not_before(self):
|
||||
return self.cert.not_valid_before
|
||||
|
||||
def _get_not_after(self):
|
||||
return self.cert.not_valid_after
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
return self.cert.public_key().public_bytes(
|
||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
try:
|
||||
ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
return ext.value.digest
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
try:
|
||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||
issuer = None
|
||||
if ext.value.authority_cert_issuer is not None:
|
||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
return None, None, None
|
||||
|
||||
def _get_serial_number(self):
|
||||
return cryptography_serial_number_of_cert(self.cert)
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return cryptography_get_extensions_from_cert(self.cert)
|
||||
|
||||
def _get_ocsp_uri(self):
|
||||
try:
|
||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
|
||||
for desc in ext.value:
|
||||
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
|
||||
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
|
||||
return desc.access_location.value
|
||||
except x509.ExtensionNotFound as dummy:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class CertificateInfoPyOpenSSL(CertificateInfo):
|
||||
"""validate the supplied certificate."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(CertificateInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def _get_signature_algorithm(self):
|
||||
return to_text(self.cert.get_signature_algorithm())
|
||||
|
||||
def __get_name(self, name):
|
||||
result = []
|
||||
for sub in name.get_components():
|
||||
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
||||
return result
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
return self.__get_name(self.cert.get_subject())
|
||||
|
||||
def _get_issuer_ordered(self):
|
||||
return self.__get_name(self.cert.get_issuer())
|
||||
|
||||
def _get_version(self):
|
||||
# Version numbers in certs are off by one:
|
||||
# v1: 0, v2: 1, v3: 2 ...
|
||||
return self.cert.get_version() + 1
|
||||
|
||||
def _get_extension(self, short_name):
|
||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
||||
extension = self.cert.get_extension(extension_idx)
|
||||
if extension.get_short_name() == short_name:
|
||||
result = [
|
||||
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
||||
]
|
||||
return sorted(result), bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_key_usage(self):
|
||||
return self._get_extension(b'keyUsage')
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
return self._get_extension(b'extendedKeyUsage')
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
return self._get_extension(b'basicConstraints')
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())]
|
||||
oms_ext = [
|
||||
ext for ext in extensions
|
||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
||||
]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if oms_ext:
|
||||
return True, bool(oms_ext[0].get_critical())
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
||||
extension = self.cert.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
return result, bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_not_before(self):
|
||||
time_string = to_native(self.cert.get_notBefore())
|
||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def _get_not_after(self):
|
||||
time_string = to_native(self.cert.get_notAfter())
|
||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.cert.get_pubkey()
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.cert.get_pubkey()._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None, None, None
|
||||
|
||||
def _get_serial_number(self):
|
||||
return self.cert.get_serial_number()
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return pyopenssl_get_extensions_from_cert(self.cert)
|
||||
|
||||
def _get_ocsp_uri(self):
|
||||
for i in range(self.cert.get_extension_count()):
|
||||
ext = self.cert.get_extension(i)
|
||||
if ext.get_short_name() == b'authorityInfoAccess':
|
||||
v = str(ext)
|
||||
m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
@@ -825,53 +412,37 @@ def main():
|
||||
module.deprecate("The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
try:
|
||||
if module.params['path'] is not None:
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
if module.params['content'] is not None:
|
||||
data = module.params['content'].encode('utf-8')
|
||||
else:
|
||||
try:
|
||||
with open(module.params['path'], 'rb') as f:
|
||||
data = f.read()
|
||||
except (IOError, OSError) as e:
|
||||
module.fail_json(msg='Error while reading certificate file from disk: {0}'.format(e))
|
||||
|
||||
backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data)
|
||||
|
||||
valid_at = module.params['valid_at']
|
||||
if valid_at:
|
||||
for k, v in valid_at.items():
|
||||
if not isinstance(v, string_types):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
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))
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detect what backend we can use
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
try:
|
||||
result = module_backend.get_info()
|
||||
|
||||
# If cryptography is available we'll use it
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
not_before = module_backend.get_not_before()
|
||||
not_after = module_backend.get_not_after()
|
||||
|
||||
# Fail if no backend has been found
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
result['valid_at'] = dict()
|
||||
if valid_at:
|
||||
for k, v in valid_at.items():
|
||||
result['valid_at'][k] = not_before <= v <= not_after
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
certificate = CertificateInfoPyOpenSSL(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
certificate = CertificateInfoCryptography(module)
|
||||
|
||||
result = certificate.get_info()
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
210
plugins/modules/x509_certificate_pipe.py
Normal file
210
plugins/modules/x509_certificate_pipe.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# Copyright: (2) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: x509_certificate_pipe
|
||||
short_description: Generate and/or check OpenSSL certificates
|
||||
version_added: 1.3.0
|
||||
description:
|
||||
- It implements a notion of provider (ie. C(selfsigned), C(ownca), C(entrust))
|
||||
for your certificate.
|
||||
- "Please note that the module regenerates an existing certificate if it does not match the module's
|
||||
options, or if it seems to be corrupt. If you are concerned that this could overwrite
|
||||
your existing certificate, consider using the I(backup) option."
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
- Markus Teufelberger (@MarkusTeufelberger)
|
||||
- Felix Fontein (@felixfontein)
|
||||
options:
|
||||
provider:
|
||||
description:
|
||||
- Name of the provider to use to generate/retrieve the OpenSSL certificate.
|
||||
- "The C(entrust) provider requires credentials for the
|
||||
L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API."
|
||||
type: str
|
||||
choices: [ entrust, ownca, selfsigned ]
|
||||
required: true
|
||||
|
||||
content:
|
||||
description:
|
||||
- The existing certificate.
|
||||
type: str
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.crypto.module_certificate
|
||||
- community.crypto.module_certificate.backend_entrust_documentation
|
||||
- community.crypto.module_certificate.backend_ownca_documentation
|
||||
- community.crypto.module_certificate.backend_selfsigned_documentation
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Generate a Self Signed OpenSSL certificate
|
||||
community.crypto.x509_certificate_pipe:
|
||||
provider: selfsigned
|
||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
||||
csr_path: /etc/ssl/csr/ansible.com.csr
|
||||
register: result
|
||||
- name: Print the certificate
|
||||
ansible.builtin.debug:
|
||||
var: result.certificate
|
||||
|
||||
# In the following example, both CSR and certificate file are stored on the
|
||||
# machine where ansible-playbook is executed, while the OwnCA data (certificate,
|
||||
# private key) are stored on the remote machine.
|
||||
|
||||
- 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') }}"
|
||||
ownca_cert: /path/to/ca_cert.crt
|
||||
ownca_privatekey: /path/to/ca_cert.key
|
||||
ownca_privatekey_passphrase: hunter2
|
||||
register: result
|
||||
|
||||
- name: (2/2) Store certificate
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/ssl/csr/www.ansible.com.crt
|
||||
content: "{{ result.certificate }}"
|
||||
delegate_to: localhost
|
||||
when: result is changed
|
||||
|
||||
# In the following example, the certificate from another machine is signed by
|
||||
# our OwnCA whose private key and certificate are only available on this
|
||||
# machine (where ansible-playbook is executed), without having to write
|
||||
# the certificate file to disk on localhost. The CSR could have been
|
||||
# provided by community.crypto.openssl_csr_pipe earlier, or also have been
|
||||
# read from the remote machine.
|
||||
|
||||
- name: (1/3) Read certificate's contents from remote machine
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/ssl/csr/www.ansible.com.crt
|
||||
register: certificate_content
|
||||
|
||||
- name: (2/3) Generate an OpenSSL Certificate with the CSR provided inline
|
||||
community.crypto.x509_certificate_pipe:
|
||||
provider: ownca
|
||||
content: "{{ certificate_content.content | b64decode }}"
|
||||
csr_content: "{{ the_csr }}"
|
||||
ownca_cert: /path/to/ca_cert.crt
|
||||
ownca_privatekey: /path/to/ca_cert.key
|
||||
ownca_privatekey_passphrase: hunter2
|
||||
delegate_to: localhost
|
||||
register: result
|
||||
|
||||
- name: (3/3) Store certificate
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/ssl/csr/www.ansible.com.crt
|
||||
content: "{{ result.certificate }}"
|
||||
when: result is changed
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
certificate:
|
||||
description: The (current or generated) certificate's content.
|
||||
returned: changed or success
|
||||
type: str
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
select_backend,
|
||||
get_certificate_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import (
|
||||
EntrustCertificateProvider,
|
||||
add_entrust_provider_to_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_ownca import (
|
||||
OwnCACertificateProvider,
|
||||
add_ownca_provider_to_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_selfsigned import (
|
||||
SelfSignedCertificateProvider,
|
||||
add_selfsigned_provider_to_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
|
||||
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_backend = module_backend
|
||||
self.changed = False
|
||||
if module.params['content'] is not None:
|
||||
self.module_backend.set_existing(module.params['content'].encode('utf-8'))
|
||||
|
||||
def generate(self, module):
|
||||
if self.module_backend.needs_regeneration():
|
||||
if not self.check_mode:
|
||||
self.module_backend.generate_certificate()
|
||||
self.changed = True
|
||||
|
||||
def dump(self, check_mode=False):
|
||||
result = self.module_backend.dump(include_certificate=True)
|
||||
result.update({
|
||||
'changed': self.changed,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = get_certificate_argument_spec()
|
||||
argument_spec.argument_spec['provider']['required'] = True
|
||||
add_entrust_provider_to_argument_spec(argument_spec)
|
||||
add_ownca_provider_to_argument_spec(argument_spec)
|
||||
add_selfsigned_provider_to_argument_spec(argument_spec)
|
||||
argument_spec.argument_spec.update(dict(
|
||||
content=dict(type='str'),
|
||||
))
|
||||
module = argument_spec.create_ansible_module(
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
try:
|
||||
provider = module.params['provider']
|
||||
provider_map = {
|
||||
'entrust': EntrustCertificateProvider,
|
||||
'ownca': OwnCACertificateProvider,
|
||||
'selfsigned': SelfSignedCertificateProvider,
|
||||
}
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
module_backend = select_backend(module, backend, provider_map[provider]())
|
||||
certificate = GenericCertificate(module, module_backend)
|
||||
certificate.generate(module)
|
||||
result = certificate.dump()
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,7 +15,7 @@ version_added: '1.0.0'
|
||||
short_description: Generate Certificate Revocation Lists (CRLs)
|
||||
description:
|
||||
- This module allows one to (re)generate or update Certificate Revocation Lists (CRLs).
|
||||
- Certificates on the revocation list can be either specified via serial number and (optionally) their issuer,
|
||||
- Certificates on the revocation list can be either specified by serial number and (optionally) their issuer,
|
||||
or as a path to a certificate file in PEM format.
|
||||
requirements:
|
||||
- cryptography >= 1.2
|
||||
@@ -233,6 +233,7 @@ extends_documentation_fragment:
|
||||
notes:
|
||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||
- Date specified should be UTC. Minutes and seconds are mandatory.
|
||||
- Supports C(check_mode).
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -259,7 +260,7 @@ EXAMPLES = r'''
|
||||
|
||||
RETURN = r'''
|
||||
filename:
|
||||
description: Path to the generated CRL
|
||||
description: Path to the generated CRL.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /path/to/my-ca.crl
|
||||
@@ -269,7 +270,7 @@ backup_file:
|
||||
type: str
|
||||
sample: /path/to/my-ca.crl.2019-03-09@11:22~
|
||||
privatekey:
|
||||
description: Path to the private CA key
|
||||
description: Path to the private CA key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /path/to/my-ca.pem
|
||||
@@ -285,13 +286,13 @@ issuer:
|
||||
- Note that for repeated values, only the last one will be returned.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
||||
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
|
||||
issuer_ordered:
|
||||
description: The CRL's issuer as an ordered list of tuples.
|
||||
returned: success
|
||||
type: list
|
||||
elements: list
|
||||
sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
|
||||
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
|
||||
last_update:
|
||||
description: The point in time from which this CRL can be trusted as ASN.1 TIME.
|
||||
returned: success
|
||||
@@ -325,7 +326,7 @@ revoked_certificates:
|
||||
description: The certificate's issuer.
|
||||
type: list
|
||||
elements: str
|
||||
sample: '["DNS:ca.example.org"]'
|
||||
sample: ["DNS:ca.example.org"]
|
||||
issuer_critical:
|
||||
description: Whether the certificate issuer extension is critical.
|
||||
type: bool
|
||||
@@ -366,10 +367,10 @@ import base64
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
write_file,
|
||||
@@ -391,6 +392,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_get_name,
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_oid_to_name,
|
||||
cryptography_serial_number_of_cert,
|
||||
@@ -404,10 +406,14 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
cryptography_get_signature_algorithm_oid_from_crl,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
identify_pem_format,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
|
||||
get_crl_info,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
@@ -501,7 +507,7 @@ class CRL(OpenSSLObject):
|
||||
result['serial_number'] = rc['serial_number']
|
||||
# All other options
|
||||
if rc['issuer']:
|
||||
result['issuer'] = [cryptography_get_name(issuer) for issuer in 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'],
|
||||
@@ -549,6 +555,19 @@ class CRL(OpenSSLObject):
|
||||
except Exception as dummy:
|
||||
self.crl_content = None
|
||||
self.actual_format = self.format
|
||||
data = None
|
||||
|
||||
self.diff_after = self.diff_before = self._get_info(data)
|
||||
|
||||
def _get_info(self, data):
|
||||
if data is None:
|
||||
return dict()
|
||||
try:
|
||||
result = get_crl_info(self.module, data)
|
||||
result['can_parse_crl'] = True
|
||||
return result
|
||||
except Exception as exc:
|
||||
return dict(can_parse_crl=False)
|
||||
|
||||
def remove(self):
|
||||
if self.backup:
|
||||
@@ -579,7 +598,7 @@ class CRL(OpenSSLObject):
|
||||
entry['invalidity_date_critical'],
|
||||
)
|
||||
|
||||
def check(self, perms_required=True, ignore_conversion=True):
|
||||
def check(self, module, perms_required=True, ignore_conversion=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
||||
@@ -594,8 +613,12 @@ class CRL(OpenSSLObject):
|
||||
return False
|
||||
if self.next_update != self.crl.next_update and not self.ignore_timestamps:
|
||||
return False
|
||||
if self.digest.name != self.crl.signature_hash_algorithm.name:
|
||||
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:
|
||||
return False
|
||||
else:
|
||||
if self.crl.signature_hash_algorithm is not None:
|
||||
return False
|
||||
|
||||
want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
|
||||
if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]:
|
||||
@@ -646,9 +669,7 @@ class CRL(OpenSSLObject):
|
||||
revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
|
||||
if entry['issuer'] is not None:
|
||||
revoked_cert = revoked_cert.add_extension(
|
||||
x509.CertificateIssuer([
|
||||
cryptography_get_name(name) for name in entry['issuer']
|
||||
]),
|
||||
x509.CertificateIssuer(entry['issuer']),
|
||||
entry['issuer_critical']
|
||||
)
|
||||
if entry['reason'] is not None:
|
||||
@@ -663,7 +684,10 @@ class CRL(OpenSSLObject):
|
||||
)
|
||||
crl = crl.add_revoked_certificate(revoked_cert.build(backend))
|
||||
|
||||
self.crl = crl.sign(self.privatekey, self.digest, backend=backend)
|
||||
digest = None
|
||||
if cryptography_key_needs_digest_for_signing(self.privatekey):
|
||||
digest = self.digest
|
||||
self.crl = crl.sign(self.privatekey, digest, backend=backend)
|
||||
if self.format == 'pem':
|
||||
return self.crl.public_bytes(Encoding.PEM)
|
||||
else:
|
||||
@@ -671,15 +695,16 @@ class CRL(OpenSSLObject):
|
||||
|
||||
def generate(self):
|
||||
result = None
|
||||
if not self.check(perms_required=False, ignore_conversion=True) or self.force:
|
||||
if not self.check(self.module, perms_required=False, ignore_conversion=True) or self.force:
|
||||
result = self._generate_crl()
|
||||
elif not self.check(perms_required=False, ignore_conversion=False) and self.crl:
|
||||
elif not self.check(self.module, perms_required=False, ignore_conversion=False) and self.crl:
|
||||
if self.format == 'pem':
|
||||
result = self.crl.public_bytes(Encoding.PEM)
|
||||
else:
|
||||
result = self.crl.public_bytes(Encoding.DER)
|
||||
|
||||
if result is not None:
|
||||
self.diff_after = self._get_info(result)
|
||||
if self.return_content:
|
||||
if self.format == 'pem':
|
||||
self.crl_content = result
|
||||
@@ -691,7 +716,9 @@ class CRL(OpenSSLObject):
|
||||
self.changed = True
|
||||
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
if self.module.set_fs_attributes_if_different(file_args, False):
|
||||
if self.module.check_file_absent_if_check_mode(file_args['path']):
|
||||
self.changed = True
|
||||
elif self.module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
|
||||
def dump(self, check_mode=False):
|
||||
@@ -741,6 +768,10 @@ class CRL(OpenSSLObject):
|
||||
if self.return_content:
|
||||
result['crl'] = self.crl_content
|
||||
|
||||
result['diff'] = dict(
|
||||
before=self.diff_before,
|
||||
after=self.diff_after,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -754,7 +785,7 @@ def main():
|
||||
path=dict(type='path', required=True),
|
||||
format=dict(type='str', default='pem', choices=['pem', 'der']),
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
issuer=dict(type='dict'),
|
||||
last_update=dict(type='str', default='+0s'),
|
||||
@@ -809,7 +840,7 @@ def main():
|
||||
if module.params['state'] == 'present':
|
||||
if module.check_mode:
|
||||
result = crl.dump(check_mode=True)
|
||||
result['changed'] = module.params['force'] or not crl.check() or not crl.check(ignore_conversion=False)
|
||||
result['changed'] = module.params['force'] or not crl.check(module) or not crl.check(module, ignore_conversion=False)
|
||||
module.exit_json(**result)
|
||||
|
||||
crl.generate()
|
||||
|
||||
@@ -30,10 +30,20 @@ options:
|
||||
- Content of the X.509 CRL in PEM format, or Base64-encoded X.509 CRL.
|
||||
- Either I(path) or I(content) must be specified, but not both.
|
||||
type: str
|
||||
list_revoked_certificates:
|
||||
description:
|
||||
- If set to C(false), the list of revoked certificates is not included in the result.
|
||||
- This is useful when retrieving information on large CRL files. Enumerating all revoked
|
||||
certificates can take some time, including serializing the result as JSON, sending it to
|
||||
the Ansible controller, and decoding it again.
|
||||
type: bool
|
||||
default: true
|
||||
version_added: 1.7.0
|
||||
|
||||
notes:
|
||||
- All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
They are all in UTC.
|
||||
- Supports C(check_mode).
|
||||
seealso:
|
||||
- module: community.crypto.x509_crl
|
||||
'''
|
||||
@@ -44,8 +54,15 @@ EXAMPLES = r'''
|
||||
path: /etc/ssl/my-ca.crl
|
||||
register: result
|
||||
|
||||
- debug:
|
||||
- name: Print the information
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ result }}"
|
||||
|
||||
- name: Get information on CRL without list of revoked certificates
|
||||
community.crypto.x509_crl_info:
|
||||
path: /etc/ssl/very-large.crl
|
||||
list_revoked_certificates: false
|
||||
register: result
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
@@ -61,23 +78,23 @@ issuer:
|
||||
- Note that for repeated values, only the last one will be returned.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
||||
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
|
||||
issuer_ordered:
|
||||
description: The CRL's issuer as an ordered list of tuples.
|
||||
returned: success
|
||||
type: list
|
||||
elements: list
|
||||
sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
|
||||
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
|
||||
last_update:
|
||||
description: The point in time from which this CRL can be trusted as ASN.1 TIME.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 20190413202428Z
|
||||
sample: '20190413202428Z'
|
||||
next_update:
|
||||
description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 20190413202428Z
|
||||
sample: '20190413202428Z'
|
||||
digest:
|
||||
description: The signature algorithm used to sign the CRL.
|
||||
returned: success
|
||||
@@ -85,7 +102,7 @@ digest:
|
||||
sample: sha256WithRSAEncryption
|
||||
revoked_certificates:
|
||||
description: List of certificates to be revoked.
|
||||
returned: success
|
||||
returned: success if I(list_revoked_certificates=true)
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
@@ -96,12 +113,12 @@ revoked_certificates:
|
||||
revocation_date:
|
||||
description: The point in time the certificate was revoked as ASN.1 TIME.
|
||||
type: str
|
||||
sample: 20190413202428Z
|
||||
sample: '20190413202428Z'
|
||||
issuer:
|
||||
description: The certificate's issuer.
|
||||
type: list
|
||||
elements: str
|
||||
sample: '["DNS:ca.example.org"]'
|
||||
sample: ["DNS:ca.example.org"]
|
||||
issuer_critical:
|
||||
description: Whether the certificate issuer extension is critical.
|
||||
type: bool
|
||||
@@ -123,7 +140,7 @@ revoked_certificates:
|
||||
The point in time it was known/suspected that the private key was compromised
|
||||
or that the certificate otherwise became invalid as ASN.1 TIME.
|
||||
type: str
|
||||
sample: 20190413202428Z
|
||||
sample: '20190413202428Z'
|
||||
invalidity_date_critical:
|
||||
description: Whether the invalidity date extension is critical.
|
||||
type: bool
|
||||
@@ -132,129 +149,22 @@ revoked_certificates:
|
||||
|
||||
|
||||
import base64
|
||||
import traceback
|
||||
import binascii
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_oid_to_name,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
|
||||
TIMESTAMP_FORMAT,
|
||||
cryptography_decode_revoked_certificate,
|
||||
cryptography_dump_revoked,
|
||||
cryptography_get_signature_algorithm_oid_from_crl,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
identify_pem_format,
|
||||
)
|
||||
|
||||
# crypto_utils
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
class CRLError(OpenSSLObjectError):
|
||||
pass
|
||||
|
||||
|
||||
class CRLInfo(OpenSSLObject):
|
||||
"""The main module implementation."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(CRLInfo, self).__init__(
|
||||
module.params['path'] or '',
|
||||
'present',
|
||||
False,
|
||||
module.check_mode
|
||||
)
|
||||
|
||||
self.content = module.params['content']
|
||||
|
||||
self.module = module
|
||||
|
||||
self.crl = None
|
||||
if self.content is None:
|
||||
try:
|
||||
with open(self.path, 'rb') as f:
|
||||
data = f.read()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
|
||||
else:
|
||||
data = self.content.encode('utf-8')
|
||||
if not identify_pem_format(data):
|
||||
data = base64.b64decode(self.content)
|
||||
|
||||
self.crl_pem = identify_pem_format(data)
|
||||
try:
|
||||
if self.crl_pem:
|
||||
self.crl = x509.load_pem_x509_crl(data, default_backend())
|
||||
else:
|
||||
self.crl = x509.load_der_x509_crl(data, default_backend())
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
||||
|
||||
def get_info(self):
|
||||
result = {
|
||||
'changed': False,
|
||||
'format': 'pem' if self.crl_pem else 'der',
|
||||
'last_update': None,
|
||||
'next_update': None,
|
||||
'digest': None,
|
||||
'issuer_ordered': None,
|
||||
'issuer': None,
|
||||
'revoked_certificates': [],
|
||||
}
|
||||
|
||||
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
||||
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
||||
result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
|
||||
issuer = []
|
||||
for attribute in self.crl.issuer:
|
||||
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||
result['issuer_ordered'] = issuer
|
||||
result['issuer'] = {}
|
||||
for k, v in issuer:
|
||||
result['issuer'][k] = v
|
||||
result['revoked_certificates'] = []
|
||||
for cert in self.crl:
|
||||
entry = cryptography_decode_revoked_certificate(cert)
|
||||
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
||||
|
||||
return result
|
||||
|
||||
def generate(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
def dump(self):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
|
||||
get_crl_info,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -262,6 +172,7 @@ def main():
|
||||
argument_spec=dict(
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str'),
|
||||
list_revoked_certificates=dict(type='bool', default=True),
|
||||
),
|
||||
required_one_of=(
|
||||
['path', 'content'],
|
||||
@@ -272,13 +183,22 @@ def main():
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
if module.params['content'] is None:
|
||||
try:
|
||||
with open(module.params['path'], 'rb') as f:
|
||||
data = f.read()
|
||||
except (IOError, OSError) as e:
|
||||
module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
|
||||
else:
|
||||
data = module.params['content'].encode('utf-8')
|
||||
if not identify_pem_format(data):
|
||||
try:
|
||||
data = base64.b64decode(module.params['content'])
|
||||
except (binascii.Error, TypeError) as e:
|
||||
module.fail_json(msg='Error while Base64 decoding content: {0}'.format(e))
|
||||
|
||||
try:
|
||||
crl = CRLInfo(module)
|
||||
result = crl.get_info()
|
||||
result = get_crl_info(module, data, list_revoked_certificates=module.params['list_revoked_certificates'])
|
||||
module.exit_json(**result)
|
||||
except OpenSSLObjectError as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
|
||||
764
plugins/plugin_utils/action_module.py
Normal file
764
plugins/plugin_utils/action_module.py
Normal file
@@ -0,0 +1,764 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2012-2013 Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Copyright (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
# Copyright (c) 2019 Ansible Project
|
||||
# Copyright (c) 2020 Felix Fontein <felix@fontein.de>
|
||||
# Copyright (c) 2021 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# Parts taken from ansible.module_utils.basic and ansible.module_utils.common.warnings.
|
||||
|
||||
# NOTE: THIS IS ONLY FOR ACTION PLUGINS!
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import AnsibleFallbackNotFound, SEQUENCETYPE, remove_values
|
||||
from ansible.module_utils.common._collections_compat import (
|
||||
Mapping
|
||||
)
|
||||
from ansible.module_utils.common.parameters import (
|
||||
PASS_VARS,
|
||||
PASS_BOOLS,
|
||||
)
|
||||
from ansible.module_utils.common.validation import (
|
||||
check_mutually_exclusive,
|
||||
check_required_arguments,
|
||||
check_required_by,
|
||||
check_required_if,
|
||||
check_required_one_of,
|
||||
check_required_together,
|
||||
count_terms,
|
||||
check_type_bool,
|
||||
check_type_bits,
|
||||
check_type_bytes,
|
||||
check_type_float,
|
||||
check_type_int,
|
||||
check_type_jsonarg,
|
||||
check_type_list,
|
||||
check_type_dict,
|
||||
check_type_path,
|
||||
check_type_raw,
|
||||
check_type_str,
|
||||
safe_eval,
|
||||
)
|
||||
from ansible.module_utils.common.text.formatters import (
|
||||
lenient_lowercase,
|
||||
)
|
||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
|
||||
from ansible.module_utils.six import (
|
||||
binary_type,
|
||||
string_types,
|
||||
text_type,
|
||||
)
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
try:
|
||||
# For ansible-core 2.11, we can use the ArgumentSpecValidator. We also import
|
||||
# ModuleArgumentSpecValidator since that indicates that the 'classical' approach
|
||||
# will no longer work.
|
||||
from ansible.module_utils.common.arg_spec import (
|
||||
ArgumentSpecValidator,
|
||||
ModuleArgumentSpecValidator, # noqa
|
||||
)
|
||||
from ansible.module_utils.errors import UnsupportedError
|
||||
HAS_ARGSPEC_VALIDATOR = True
|
||||
except ImportError:
|
||||
# For ansible-base 2.10 and Ansible 2.9, we need to use the 'classical' approach
|
||||
from ansible.module_utils.common.parameters import (
|
||||
handle_aliases,
|
||||
list_deprecations,
|
||||
list_no_log_values,
|
||||
)
|
||||
HAS_ARGSPEC_VALIDATOR = False
|
||||
|
||||
|
||||
class _ModuleExitException(Exception):
|
||||
def __init__(self, result):
|
||||
super(_ModuleExitException, self).__init__()
|
||||
self.result = result
|
||||
|
||||
|
||||
class AnsibleActionModule(object):
|
||||
def __init__(self, action_plugin, argument_spec, bypass_checks=False,
|
||||
mutually_exclusive=None, required_together=None,
|
||||
required_one_of=None, supports_check_mode=False,
|
||||
required_if=None, required_by=None):
|
||||
# Internal data
|
||||
self.__action_plugin = action_plugin
|
||||
self.__warnings = []
|
||||
self.__deprecations = []
|
||||
|
||||
# AnsibleModule data
|
||||
self._name = self.__action_plugin._task.action
|
||||
self.argument_spec = argument_spec
|
||||
self.supports_check_mode = supports_check_mode
|
||||
self.check_mode = self.__action_plugin._play_context.check_mode
|
||||
self.bypass_checks = bypass_checks
|
||||
self.no_log = self.__action_plugin._play_context.no_log
|
||||
|
||||
self.mutually_exclusive = mutually_exclusive
|
||||
self.required_together = required_together
|
||||
self.required_one_of = required_one_of
|
||||
self.required_if = required_if
|
||||
self.required_by = required_by
|
||||
self._diff = self.__action_plugin._play_context.diff
|
||||
self._verbosity = self.__action_plugin._display.verbosity
|
||||
|
||||
self.aliases = {}
|
||||
self._legal_inputs = []
|
||||
self._options_context = list()
|
||||
|
||||
self.params = copy.deepcopy(self.__action_plugin._task.args)
|
||||
self.no_log_values = set()
|
||||
if HAS_ARGSPEC_VALIDATOR:
|
||||
self._validator = ArgumentSpecValidator(
|
||||
self.argument_spec,
|
||||
self.mutually_exclusive,
|
||||
self.required_together,
|
||||
self.required_one_of,
|
||||
self.required_if,
|
||||
self.required_by,
|
||||
)
|
||||
self._validation_result = self._validator.validate(self.params)
|
||||
self.params.update(self._validation_result.validated_parameters)
|
||||
self.no_log_values.update(self._validation_result._no_log_values)
|
||||
|
||||
try:
|
||||
error = self._validation_result.errors[0]
|
||||
except IndexError:
|
||||
error = None
|
||||
|
||||
# We cannot use ModuleArgumentSpecValidator directly since it uses mechanisms for reporting
|
||||
# warnings and deprecations that do not work in plugins. This is a copy of that code adjusted
|
||||
# for our use-case:
|
||||
for d in self._validation_result._deprecations:
|
||||
# Before ansible-core 2.14.2, deprecations were always for aliases:
|
||||
if 'name' in d:
|
||||
self.deprecate(
|
||||
"Alias '{name}' is deprecated. See the module docs for more information".format(name=d['name']),
|
||||
version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name'))
|
||||
# Since ansible-core 2.14.2, a message is present that can be directly printed:
|
||||
if 'msg' in d:
|
||||
self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name'))
|
||||
|
||||
for w in self._validation_result._warnings:
|
||||
self.warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias']))
|
||||
|
||||
# Fail for validation errors, even in check mode
|
||||
if error:
|
||||
msg = self._validation_result.errors.msg
|
||||
if isinstance(error, UnsupportedError):
|
||||
msg = "Unsupported parameters for ({name}) {kind}: {msg}".format(name=self._name, kind='module', msg=msg)
|
||||
|
||||
self.fail_json(msg=msg)
|
||||
else:
|
||||
self._set_fallbacks()
|
||||
|
||||
# append to legal_inputs and then possibly check against them
|
||||
try:
|
||||
self.aliases = self._handle_aliases()
|
||||
except (ValueError, TypeError) as e:
|
||||
# Use exceptions here because it isn't safe to call fail_json until no_log is processed
|
||||
raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e)))
|
||||
|
||||
# Save parameter values that should never be logged
|
||||
self._handle_no_log_values()
|
||||
|
||||
self._check_arguments()
|
||||
|
||||
# check exclusive early
|
||||
if not bypass_checks:
|
||||
self._check_mutually_exclusive(mutually_exclusive)
|
||||
|
||||
self._set_defaults(pre=True)
|
||||
|
||||
self._CHECK_ARGUMENT_TYPES_DISPATCHER = {
|
||||
'str': self._check_type_str,
|
||||
'list': check_type_list,
|
||||
'dict': check_type_dict,
|
||||
'bool': check_type_bool,
|
||||
'int': check_type_int,
|
||||
'float': check_type_float,
|
||||
'path': check_type_path,
|
||||
'raw': check_type_raw,
|
||||
'jsonarg': check_type_jsonarg,
|
||||
'json': check_type_jsonarg,
|
||||
'bytes': check_type_bytes,
|
||||
'bits': check_type_bits,
|
||||
}
|
||||
if not bypass_checks:
|
||||
self._check_required_arguments()
|
||||
self._check_argument_types()
|
||||
self._check_argument_values()
|
||||
self._check_required_together(required_together)
|
||||
self._check_required_one_of(required_one_of)
|
||||
self._check_required_if(required_if)
|
||||
self._check_required_by(required_by)
|
||||
|
||||
self._set_defaults(pre=False)
|
||||
|
||||
# deal with options sub-spec
|
||||
self._handle_options()
|
||||
|
||||
def _handle_aliases(self, spec=None, param=None, option_prefix=''):
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
# this uses exceptions as it happens before we can safely call fail_json
|
||||
alias_warnings = []
|
||||
alias_results, self._legal_inputs = handle_aliases(spec, param, alias_warnings=alias_warnings)
|
||||
for option, alias in alias_warnings:
|
||||
self.warn('Both option %s and its alias %s are set.' % (option_prefix + option, option_prefix + alias))
|
||||
|
||||
deprecated_aliases = []
|
||||
for i in spec.keys():
|
||||
if 'deprecated_aliases' in spec[i].keys():
|
||||
for alias in spec[i]['deprecated_aliases']:
|
||||
deprecated_aliases.append(alias)
|
||||
|
||||
for deprecation in deprecated_aliases:
|
||||
if deprecation['name'] in param.keys():
|
||||
self.deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'],
|
||||
version=deprecation.get('version'), date=deprecation.get('date'),
|
||||
collection_name=deprecation.get('collection_name'))
|
||||
return alias_results
|
||||
|
||||
def _handle_no_log_values(self, spec=None, param=None):
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
self.no_log_values.update(list_no_log_values(spec, param))
|
||||
except TypeError as te:
|
||||
self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. "
|
||||
"%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'})
|
||||
|
||||
for message in list_deprecations(spec, param):
|
||||
self.deprecate(message['msg'], version=message.get('version'), date=message.get('date'),
|
||||
collection_name=message.get('collection_name'))
|
||||
|
||||
def _check_arguments(self, spec=None, param=None, legal_inputs=None):
|
||||
self._syslog_facility = 'LOG_USER'
|
||||
unsupported_parameters = set()
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
if legal_inputs is None:
|
||||
legal_inputs = self._legal_inputs
|
||||
|
||||
for k in list(param.keys()):
|
||||
|
||||
if k not in legal_inputs:
|
||||
unsupported_parameters.add(k)
|
||||
|
||||
for k in PASS_VARS:
|
||||
# handle setting internal properties from internal ansible vars
|
||||
param_key = '_ansible_%s' % k
|
||||
if param_key in param:
|
||||
if k in PASS_BOOLS:
|
||||
setattr(self, PASS_VARS[k][0], self.boolean(param[param_key]))
|
||||
else:
|
||||
setattr(self, PASS_VARS[k][0], param[param_key])
|
||||
|
||||
# clean up internal top level params:
|
||||
if param_key in self.params:
|
||||
del self.params[param_key]
|
||||
else:
|
||||
# use defaults if not already set
|
||||
if not hasattr(self, PASS_VARS[k][0]):
|
||||
setattr(self, PASS_VARS[k][0], PASS_VARS[k][1])
|
||||
|
||||
if unsupported_parameters:
|
||||
msg = "Unsupported parameters for (%s) module: %s" % (self._name, ', '.join(sorted(list(unsupported_parameters))))
|
||||
if self._options_context:
|
||||
msg += " found in %s." % " -> ".join(self._options_context)
|
||||
supported_parameters = list()
|
||||
for key in sorted(spec.keys()):
|
||||
if 'aliases' in spec[key] and spec[key]['aliases']:
|
||||
supported_parameters.append("%s (%s)" % (key, ', '.join(sorted(spec[key]['aliases']))))
|
||||
else:
|
||||
supported_parameters.append(key)
|
||||
msg += " Supported parameters include: %s" % (', '.join(supported_parameters))
|
||||
self.fail_json(msg=msg)
|
||||
if self.check_mode and not self.supports_check_mode:
|
||||
self.exit_json(skipped=True, msg="action module (%s) does not support check mode" % self._name)
|
||||
|
||||
def _count_terms(self, check, param=None):
|
||||
if param is None:
|
||||
param = self.params
|
||||
return count_terms(check, param)
|
||||
|
||||
def _check_mutually_exclusive(self, spec, param=None):
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
check_mutually_exclusive(spec, param)
|
||||
except TypeError as e:
|
||||
msg = to_native(e)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def _check_required_one_of(self, spec, param=None):
|
||||
if spec is None:
|
||||
return
|
||||
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
check_required_one_of(spec, param)
|
||||
except TypeError as e:
|
||||
msg = to_native(e)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def _check_required_together(self, spec, param=None):
|
||||
if spec is None:
|
||||
return
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
check_required_together(spec, param)
|
||||
except TypeError as e:
|
||||
msg = to_native(e)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def _check_required_by(self, spec, param=None):
|
||||
if spec is None:
|
||||
return
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
check_required_by(spec, param)
|
||||
except TypeError as e:
|
||||
self.fail_json(msg=to_native(e))
|
||||
|
||||
def _check_required_arguments(self, spec=None, param=None):
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
check_required_arguments(spec, param)
|
||||
except TypeError as e:
|
||||
msg = to_native(e)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def _check_required_if(self, spec, param=None):
|
||||
''' ensure that parameters which conditionally required are present '''
|
||||
if spec is None:
|
||||
return
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
try:
|
||||
check_required_if(spec, param)
|
||||
except TypeError as e:
|
||||
msg = to_native(e)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def _check_argument_values(self, spec=None, param=None):
|
||||
''' ensure all arguments have the requested values, and there are no stray arguments '''
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
for (k, v) in spec.items():
|
||||
choices = v.get('choices', None)
|
||||
if choices is None:
|
||||
continue
|
||||
if isinstance(choices, SEQUENCETYPE) and not isinstance(choices, (binary_type, text_type)):
|
||||
if k in param:
|
||||
# Allow one or more when type='list' param with choices
|
||||
if isinstance(param[k], list):
|
||||
diff_list = ", ".join([item for item in param[k] if item not in choices])
|
||||
if diff_list:
|
||||
choices_str = ", ".join([to_native(c) for c in choices])
|
||||
msg = "value of %s must be one or more of: %s. Got no match for: %s" % (k, choices_str, diff_list)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
elif param[k] not in choices:
|
||||
# PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking
|
||||
# the value. If we can't figure this out, module author is responsible.
|
||||
lowered_choices = None
|
||||
if param[k] == 'False':
|
||||
lowered_choices = lenient_lowercase(choices)
|
||||
overlap = BOOLEANS_FALSE.intersection(choices)
|
||||
if len(overlap) == 1:
|
||||
# Extract from a set
|
||||
(param[k],) = overlap
|
||||
|
||||
if param[k] == 'True':
|
||||
if lowered_choices is None:
|
||||
lowered_choices = lenient_lowercase(choices)
|
||||
overlap = BOOLEANS_TRUE.intersection(choices)
|
||||
if len(overlap) == 1:
|
||||
(param[k],) = overlap
|
||||
|
||||
if param[k] not in choices:
|
||||
choices_str = ", ".join([to_native(c) for c in choices])
|
||||
msg = "value of %s must be one of: %s, got: %s" % (k, choices_str, param[k])
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
else:
|
||||
msg = "internal error: choices for argument %s are not iterable: %s" % (k, choices)
|
||||
if self._options_context:
|
||||
msg += " found in %s" % " -> ".join(self._options_context)
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def safe_eval(self, value, locals=None, include_exceptions=False):
|
||||
return safe_eval(value, locals, include_exceptions)
|
||||
|
||||
def _check_type_str(self, value, param=None, prefix=''):
|
||||
opts = {
|
||||
'error': False,
|
||||
'warn': False,
|
||||
'ignore': True
|
||||
}
|
||||
|
||||
# Ignore, warn, or error when converting to a string.
|
||||
allow_conversion = opts.get(C.STRING_CONVERSION_ACTION, True)
|
||||
try:
|
||||
return check_type_str(value, allow_conversion)
|
||||
except TypeError:
|
||||
common_msg = 'quote the entire value to ensure it does not change.'
|
||||
from_msg = '{0!r}'.format(value)
|
||||
to_msg = '{0!r}'.format(to_text(value))
|
||||
|
||||
if param is not None:
|
||||
if prefix:
|
||||
param = '{0}{1}'.format(prefix, param)
|
||||
|
||||
from_msg = '{0}: {1!r}'.format(param, value)
|
||||
to_msg = '{0}: {1!r}'.format(param, to_text(value))
|
||||
|
||||
if C.STRING_CONVERSION_ACTION == 'error':
|
||||
msg = common_msg.capitalize()
|
||||
raise TypeError(to_native(msg))
|
||||
elif C.STRING_CONVERSION_ACTION == 'warn':
|
||||
msg = ('The value "{0}" (type {1.__class__.__name__}) was converted to "{2}" (type string). '
|
||||
'If this does not look like what you expect, {3}').format(from_msg, value, to_msg, common_msg)
|
||||
self.warn(to_native(msg))
|
||||
return to_native(value, errors='surrogate_or_strict')
|
||||
|
||||
def _handle_options(self, argument_spec=None, params=None, prefix=''):
|
||||
''' deal with options to create sub spec '''
|
||||
if argument_spec is None:
|
||||
argument_spec = self.argument_spec
|
||||
if params is None:
|
||||
params = self.params
|
||||
|
||||
for (k, v) in argument_spec.items():
|
||||
wanted = v.get('type', None)
|
||||
if wanted == 'dict' or (wanted == 'list' and v.get('elements', '') == 'dict'):
|
||||
spec = v.get('options', None)
|
||||
if v.get('apply_defaults', False):
|
||||
if spec is not None:
|
||||
if params.get(k) is None:
|
||||
params[k] = {}
|
||||
else:
|
||||
continue
|
||||
elif spec is None or k not in params or params[k] is None:
|
||||
continue
|
||||
|
||||
self._options_context.append(k)
|
||||
|
||||
if isinstance(params[k], dict):
|
||||
elements = [params[k]]
|
||||
else:
|
||||
elements = params[k]
|
||||
|
||||
for idx, param in enumerate(elements):
|
||||
if not isinstance(param, dict):
|
||||
self.fail_json(msg="value of %s must be of type dict or list of dict" % k)
|
||||
|
||||
new_prefix = prefix + k
|
||||
if wanted == 'list':
|
||||
new_prefix += '[%d]' % idx
|
||||
new_prefix += '.'
|
||||
|
||||
self._set_fallbacks(spec, param)
|
||||
options_aliases = self._handle_aliases(spec, param, option_prefix=new_prefix)
|
||||
|
||||
options_legal_inputs = list(spec.keys()) + list(options_aliases.keys())
|
||||
|
||||
self._check_arguments(spec, param, options_legal_inputs)
|
||||
|
||||
# check exclusive early
|
||||
if not self.bypass_checks:
|
||||
self._check_mutually_exclusive(v.get('mutually_exclusive', None), param)
|
||||
|
||||
self._set_defaults(pre=True, spec=spec, param=param)
|
||||
|
||||
if not self.bypass_checks:
|
||||
self._check_required_arguments(spec, param)
|
||||
self._check_argument_types(spec, param, new_prefix)
|
||||
self._check_argument_values(spec, param)
|
||||
|
||||
self._check_required_together(v.get('required_together', None), param)
|
||||
self._check_required_one_of(v.get('required_one_of', None), param)
|
||||
self._check_required_if(v.get('required_if', None), param)
|
||||
self._check_required_by(v.get('required_by', None), param)
|
||||
|
||||
self._set_defaults(pre=False, spec=spec, param=param)
|
||||
|
||||
# handle multi level options (sub argspec)
|
||||
self._handle_options(spec, param, new_prefix)
|
||||
self._options_context.pop()
|
||||
|
||||
def _get_wanted_type(self, wanted, k):
|
||||
if not callable(wanted):
|
||||
if wanted is None:
|
||||
# Mostly we want to default to str.
|
||||
# For values set to None explicitly, return None instead as
|
||||
# that allows a user to unset a parameter
|
||||
wanted = 'str'
|
||||
try:
|
||||
type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted]
|
||||
except KeyError:
|
||||
self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
|
||||
else:
|
||||
# set the type_checker to the callable, and reset wanted to the callable's name (or type if it doesn't have one, ala MagicMock)
|
||||
type_checker = wanted
|
||||
wanted = getattr(wanted, '__name__', to_native(type(wanted)))
|
||||
|
||||
return type_checker, wanted
|
||||
|
||||
def _handle_elements(self, wanted, param, values):
|
||||
type_checker, wanted_name = self._get_wanted_type(wanted, param)
|
||||
validated_params = []
|
||||
# Get param name for strings so we can later display this value in a useful error message if needed
|
||||
# Only pass 'kwargs' to our checkers and ignore custom callable checkers
|
||||
kwargs = {}
|
||||
if wanted_name == 'str' and isinstance(wanted, string_types):
|
||||
if isinstance(param, string_types):
|
||||
kwargs['param'] = param
|
||||
elif isinstance(param, dict):
|
||||
kwargs['param'] = list(param.keys())[0]
|
||||
for value in values:
|
||||
try:
|
||||
validated_params.append(type_checker(value, **kwargs))
|
||||
except (TypeError, ValueError) as e:
|
||||
msg = "Elements value for option %s" % param
|
||||
if self._options_context:
|
||||
msg += " found in '%s'" % " -> ".join(self._options_context)
|
||||
msg += " is of type %s and we were unable to convert to %s: %s" % (type(value), wanted_name, to_native(e))
|
||||
self.fail_json(msg=msg)
|
||||
return validated_params
|
||||
|
||||
def _check_argument_types(self, spec=None, param=None, prefix=''):
|
||||
''' ensure all arguments have the requested type '''
|
||||
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
for (k, v) in spec.items():
|
||||
wanted = v.get('type', None)
|
||||
if k not in param:
|
||||
continue
|
||||
|
||||
value = param[k]
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
type_checker, wanted_name = self._get_wanted_type(wanted, k)
|
||||
# Get param name for strings so we can later display this value in a useful error message if needed
|
||||
# Only pass 'kwargs' to our checkers and ignore custom callable checkers
|
||||
kwargs = {}
|
||||
if wanted_name == 'str' and isinstance(type_checker, string_types):
|
||||
kwargs['param'] = list(param.keys())[0]
|
||||
|
||||
# Get the name of the parent key if this is a nested option
|
||||
if prefix:
|
||||
kwargs['prefix'] = prefix
|
||||
|
||||
try:
|
||||
param[k] = type_checker(value, **kwargs)
|
||||
wanted_elements = v.get('elements', None)
|
||||
if wanted_elements:
|
||||
if wanted != 'list' or not isinstance(param[k], list):
|
||||
msg = "Invalid type %s for option '%s'" % (wanted_name, param)
|
||||
if self._options_context:
|
||||
msg += " found in '%s'." % " -> ".join(self._options_context)
|
||||
msg += ", elements value check is supported only with 'list' type"
|
||||
self.fail_json(msg=msg)
|
||||
param[k] = self._handle_elements(wanted_elements, k, param[k])
|
||||
|
||||
except (TypeError, ValueError) as e:
|
||||
msg = "argument %s is of type %s" % (k, type(value))
|
||||
if self._options_context:
|
||||
msg += " found in '%s'." % " -> ".join(self._options_context)
|
||||
msg += " and we were unable to convert to %s: %s" % (wanted_name, to_native(e))
|
||||
self.fail_json(msg=msg)
|
||||
|
||||
def _set_defaults(self, pre=True, spec=None, param=None):
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
for (k, v) in spec.items():
|
||||
default = v.get('default', None)
|
||||
if pre is True:
|
||||
# this prevents setting defaults on required items
|
||||
if default is not None and k not in param:
|
||||
param[k] = default
|
||||
else:
|
||||
# make sure things without a default still get set None
|
||||
if k not in param:
|
||||
param[k] = default
|
||||
|
||||
def _set_fallbacks(self, spec=None, param=None):
|
||||
if spec is None:
|
||||
spec = self.argument_spec
|
||||
if param is None:
|
||||
param = self.params
|
||||
|
||||
for (k, v) in spec.items():
|
||||
fallback = v.get('fallback', (None,))
|
||||
fallback_strategy = fallback[0]
|
||||
fallback_args = []
|
||||
fallback_kwargs = {}
|
||||
if k not in param and fallback_strategy is not None:
|
||||
for item in fallback[1:]:
|
||||
if isinstance(item, dict):
|
||||
fallback_kwargs = item
|
||||
else:
|
||||
fallback_args = item
|
||||
try:
|
||||
param[k] = fallback_strategy(*fallback_args, **fallback_kwargs)
|
||||
except AnsibleFallbackNotFound:
|
||||
continue
|
||||
|
||||
def warn(self, warning):
|
||||
# Copied from ansible.module_utils.common.warnings:
|
||||
if isinstance(warning, string_types):
|
||||
self.__warnings.append(warning)
|
||||
else:
|
||||
raise TypeError("warn requires a string not a %s" % type(warning))
|
||||
|
||||
def deprecate(self, msg, version=None, date=None, collection_name=None):
|
||||
if version is not None and date is not None:
|
||||
raise AssertionError("implementation error -- version and date must not both be set")
|
||||
|
||||
# Copied from ansible.module_utils.common.warnings:
|
||||
if isinstance(msg, string_types):
|
||||
# For compatibility, we accept that neither version nor date is set,
|
||||
# and treat that the same as if version would haven been set
|
||||
if date is not None:
|
||||
self.__deprecations.append({'msg': msg, 'date': date, 'collection_name': collection_name})
|
||||
else:
|
||||
self.__deprecations.append({'msg': msg, 'version': version, 'collection_name': collection_name})
|
||||
else:
|
||||
raise TypeError("deprecate requires a string not a %s" % type(msg))
|
||||
|
||||
def _return_formatted(self, kwargs):
|
||||
if 'invocation' not in kwargs:
|
||||
kwargs['invocation'] = {'module_args': self.params}
|
||||
|
||||
if 'warnings' in kwargs:
|
||||
if isinstance(kwargs['warnings'], list):
|
||||
for w in kwargs['warnings']:
|
||||
self.warn(w)
|
||||
else:
|
||||
self.warn(kwargs['warnings'])
|
||||
|
||||
if self.__warnings:
|
||||
kwargs['warnings'] = self.__warnings
|
||||
|
||||
if 'deprecations' in kwargs:
|
||||
if isinstance(kwargs['deprecations'], list):
|
||||
for d in kwargs['deprecations']:
|
||||
if isinstance(d, SEQUENCETYPE) and len(d) == 2:
|
||||
self.deprecate(d[0], version=d[1])
|
||||
elif isinstance(d, Mapping):
|
||||
self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'),
|
||||
collection_name=d.get('collection_name'))
|
||||
else:
|
||||
self.deprecate(d) # pylint: disable=ansible-deprecated-no-version
|
||||
else:
|
||||
self.deprecate(kwargs['deprecations']) # pylint: disable=ansible-deprecated-no-version
|
||||
|
||||
if self.__deprecations:
|
||||
kwargs['deprecations'] = self.__deprecations
|
||||
|
||||
kwargs = remove_values(kwargs, self.no_log_values)
|
||||
raise _ModuleExitException(kwargs)
|
||||
|
||||
def exit_json(self, **kwargs):
|
||||
result = dict(kwargs)
|
||||
if 'failed' not in result:
|
||||
result['failed'] = False
|
||||
self._return_formatted(result)
|
||||
|
||||
def fail_json(self, msg, **kwargs):
|
||||
result = dict(kwargs)
|
||||
result['failed'] = True
|
||||
result['msg'] = msg
|
||||
self._return_formatted(result)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ActionModuleBase(ActionBase):
|
||||
@abc.abstractmethod
|
||||
def setup_module(self):
|
||||
"""Return pair (ArgumentSpec, kwargs)."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def run_module(self, module):
|
||||
"""Run module code"""
|
||||
module.fail_json(msg='Not implemented.')
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
if task_vars is None:
|
||||
task_vars = dict()
|
||||
|
||||
result = super(ActionModuleBase, self).run(tmp, task_vars)
|
||||
del tmp # tmp no longer has any effect
|
||||
|
||||
try:
|
||||
argument_spec, kwargs = self.setup_module()
|
||||
module = argument_spec.create_ansible_module_helper(AnsibleActionModule, (self, ), **kwargs)
|
||||
self.run_module(module)
|
||||
raise AnsibleError('Internal error: action module did not call module.exit_json()')
|
||||
except _ModuleExitException as mee:
|
||||
result.update(mee.result)
|
||||
return result
|
||||
except Exception as dummy:
|
||||
result['failed'] = True
|
||||
result['msg'] = 'MODULE FAILURE'
|
||||
result['exception'] = traceback.format_exc()
|
||||
return result
|
||||
@@ -1,72 +0,0 @@
|
||||
language: python
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- T=none
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- env: T=none
|
||||
include:
|
||||
- env: T=devel/sanity/1
|
||||
- env: T=devel/sanity/extra
|
||||
|
||||
- env: T=devel/units/2.7/1
|
||||
- env: T=devel/units/3.5/1
|
||||
- env: T=devel/units/3.6/1
|
||||
- env: T=devel/units/3.7/1
|
||||
- env: T=devel/units/3.8/1
|
||||
- env: T=devel/units/3.9/1
|
||||
|
||||
- env: T=devel/osx/10.11/1
|
||||
- env: T=devel/rhel/7.8/1
|
||||
- env: T=devel/rhel/8.2/1
|
||||
- env: T=devel/freebsd/11.1/1
|
||||
- env: T=devel/freebsd/12.1/1
|
||||
- env: T=devel/linux/centos6/1
|
||||
- env: T=devel/linux/centos7/1
|
||||
- env: T=devel/linux/centos8/1
|
||||
- env: T=devel/linux/fedora30/1
|
||||
- env: T=devel/linux/fedora31/1
|
||||
- env: T=devel/linux/fedora32/1
|
||||
- env: T=devel/linux/opensuse15py2/1
|
||||
- env: T=devel/linux/opensuse15/1
|
||||
- env: T=devel/linux/ubuntu1604/1
|
||||
- env: T=devel/linux/ubuntu1804/1
|
||||
|
||||
- env: T=devel/cloud/2.7/1
|
||||
- env: T=devel/cloud/3.6/1
|
||||
|
||||
# For Ansible 2.10, use a combination of different targets
|
||||
- env: T=2.10/sanity/1
|
||||
- env: T=2.10/units/2.7/1
|
||||
- env: T=2.10/units/3.8/1
|
||||
- env: T=2.10/rhel/7.8/1
|
||||
- env: T=2.10/linux/ubuntu1804/1
|
||||
- env: T=2.10/cloud/3.6/1
|
||||
|
||||
# For Ansible 2.9, use a combination of different targets
|
||||
- env: T=2.9/sanity/1
|
||||
- env: T=2.9/units/2.7/1
|
||||
- env: T=2.9/units/3.8/1
|
||||
- env: T=2.9/rhel/7.8/1
|
||||
- env: T=2.9/linux/ubuntu1804/1
|
||||
- env: T=2.9/cloud/3.6/1
|
||||
|
||||
branches:
|
||||
except:
|
||||
- "*-patch-*"
|
||||
- "revert-*-*"
|
||||
|
||||
build:
|
||||
ci:
|
||||
- tests/utils/shippable/timing.sh tests/utils/shippable/shippable.sh $T
|
||||
|
||||
integrations:
|
||||
notifications:
|
||||
- integrationName: email
|
||||
type: email
|
||||
on_success: never
|
||||
on_failure: never
|
||||
on_start: never
|
||||
on_pull_request: never
|
||||
8
simplified_bsd.txt
Normal file
8
simplified_bsd.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user