mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
694 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e58fe63dde | ||
|
|
0d1f260328 | ||
|
|
ba55ba7381 | ||
|
|
260bdb1572 | ||
|
|
7d5ebad188 | ||
|
|
78d9fe5813 | ||
|
|
a42e541326 | ||
|
|
673b18d9a9 | ||
|
|
2a99218162 | ||
|
|
e1763e22ae | ||
|
|
2433fdab98 | ||
|
|
cb6edf1a5f | ||
|
|
3d4c5346c6 | ||
|
|
a8aa05ac4e | ||
|
|
0e122e5f56 | ||
|
|
47ea1af180 | ||
|
|
3951e6ceb4 | ||
|
|
bf70f8d717 | ||
|
|
214794d056 | ||
|
|
b9fa5b5193 | ||
|
|
5366b9e5ba | ||
|
|
fd67767538 | ||
|
|
ae35be3437 | ||
|
|
01e7bf1f33 | ||
|
|
49354f2121 | ||
|
|
072318466e | ||
|
|
248250514f | ||
|
|
2419e6c6ad | ||
|
|
029e009db1 | ||
|
|
cfd524f345 | ||
|
|
355480601d | ||
|
|
f956ddcc77 | ||
|
|
ccb4ecfbd8 | ||
|
|
95886d1cf9 | ||
|
|
9b53f4b382 | ||
|
|
3f0e292246 | ||
|
|
0d4b16aadb | ||
|
|
db04914ab6 | ||
|
|
abb0d67774 | ||
|
|
05c442ab5e | ||
|
|
4ce9745d35 | ||
|
|
37af200ecb | ||
|
|
ddbcf49868 | ||
|
|
942be86635 | ||
|
|
2ed7f69b83 | ||
|
|
91504cda85 | ||
|
|
16434d9ad8 | ||
|
|
9e10cfb53a | ||
|
|
32047dccc5 | ||
|
|
0f7c5f0de1 | ||
|
|
8b831dbe59 | ||
|
|
8e33aafdba | ||
|
|
1b134f2d13 | ||
|
|
7adca3efff | ||
|
|
6731b38baa | ||
|
|
feee571bc8 | ||
|
|
21e344e283 | ||
|
|
7c93b61532 | ||
|
|
dd8b90f9d3 | ||
|
|
e1c0ab5bd2 | ||
|
|
a57be5ceb3 | ||
|
|
6d4a8435c7 | ||
|
|
a39b3bc882 | ||
|
|
30a16c8f60 | ||
|
|
0638512cf9 | ||
|
|
4ee90e5ea2 | ||
|
|
9cdd28c2ca | ||
|
|
db871c2686 | ||
|
|
5a2dff7b74 | ||
|
|
2d82f49adc | ||
|
|
1095c0be41 | ||
|
|
019b0fd725 | ||
|
|
80c129941a | ||
|
|
eeb6152703 | ||
|
|
e140642ba4 | ||
|
|
a49711d383 | ||
|
|
f0b8073ea5 | ||
|
|
49f64aecee | ||
|
|
dc49cc6e26 | ||
|
|
e42f8e0d0c | ||
|
|
114a29f4ea | ||
|
|
bb1cdef4c6 | ||
|
|
cd0444bd53 | ||
|
|
cb3f55076e | ||
|
|
e1e60892a8 | ||
|
|
d509af540d | ||
|
|
c8767ede77 | ||
|
|
f7c0a85c72 | ||
|
|
8935ab8fdc | ||
|
|
1f39b0ff2a | ||
|
|
b02fb8e9a0 | ||
|
|
d50c3cc944 | ||
|
|
4c26fada5e | ||
|
|
d13d1868b6 | ||
|
|
6a0953b19f | ||
|
|
6ba06f24ce | ||
|
|
577d86265e | ||
|
|
1c1b59b719 | ||
|
|
518847a92c | ||
|
|
aa30b4c803 | ||
|
|
a9dab608c7 | ||
|
|
e6643fd2dd | ||
|
|
f58606b64d | ||
|
|
5e60bee9c0 | ||
|
|
33410b1d57 | ||
|
|
e365ae3226 | ||
|
|
5f6e0095b0 | ||
|
|
dc052bee21 | ||
|
|
38849514f3 | ||
|
|
7810e2c3bf | ||
|
|
5d4cbbb038 | ||
|
|
58a81374d6 | ||
|
|
c29c34bab2 | ||
|
|
b4452d4be1 | ||
|
|
7fc3ad0263 | ||
|
|
65ea02a73d | ||
|
|
00d23753ca | ||
|
|
3d8c68e189 | ||
|
|
d7a0723a52 | ||
|
|
67bf3a7991 | ||
|
|
82251c2d80 | ||
|
|
f43fa94549 | ||
|
|
29ac3cbe81 | ||
|
|
5e59c5261e | ||
|
|
aa82575a78 | ||
|
|
f3c9cb7a8a | ||
|
|
f82b335916 | ||
|
|
553ab45f46 | ||
|
|
59606d48ad | ||
|
|
0a15be1017 | ||
|
|
9501a28a93 | ||
|
|
d906914737 | ||
|
|
33d278ad8f | ||
|
|
6d4fc589ae | ||
|
|
9614b09f7a | ||
|
|
af5f4b57f8 | ||
|
|
c6fbe58382 | ||
|
|
afe7f7522c | ||
|
|
0c62837296 | ||
|
|
d71637c77d | ||
|
|
3899f79f97 | ||
|
|
8ce0051f9b | ||
|
|
4be691da50 | ||
|
|
8fe012cf09 | ||
|
|
27a9ff14fb | ||
|
|
ae548de502 | ||
|
|
1b75f1aa9c | ||
|
|
7e33398d5c | ||
|
|
50c2c4db29 | ||
|
|
ee0ceea118 | ||
|
|
b98cec74ae | ||
|
|
05cc5fe82b | ||
|
|
fad3c1352b | ||
|
|
4167d2c4b3 | ||
|
|
ff1504dc58 | ||
|
|
08adb6b297 | ||
|
|
42ba0a88f4 | ||
|
|
1736602ce7 | ||
|
|
6b1a3d6e68 | ||
|
|
51591891d3 | ||
|
|
d1a229c255 | ||
|
|
d9698a6eff | ||
|
|
37fed289e6 | ||
|
|
9ec8680936 | ||
|
|
87af1f2761 | ||
|
|
da30487119 | ||
|
|
b57aa4a2ca | ||
|
|
a5f5ea1128 | ||
|
|
91dd7cd4dc | ||
|
|
2913826352 | ||
|
|
0bc15598d7 | ||
|
|
fb3f68ca96 | ||
|
|
a4edf22a9c | ||
|
|
97e44c4ba5 | ||
|
|
453adb5d04 | ||
|
|
033b456b7a | ||
|
|
73dbb84fc6 | ||
|
|
780fb28946 | ||
|
|
815ce43d17 | ||
|
|
170d837122 | ||
|
|
b5269b25a3 | ||
|
|
f12e814344 | ||
|
|
5d5a21fddf | ||
|
|
67f1d1129b | ||
|
|
d9362a2ce9 | ||
|
|
4e5966e477 | ||
|
|
22e24f24c6 | ||
|
|
35b47f73f4 | ||
|
|
9cc1731767 | ||
|
|
c592eaa35a | ||
|
|
525a8a5df4 | ||
|
|
e4ba0861e5 | ||
|
|
29cd0b3bde | ||
|
|
f2ebae635a | ||
|
|
75934cdd8c | ||
|
|
cf1fe027dd | ||
|
|
e9dbc1a5a5 | ||
|
|
6bd5eee9b0 | ||
|
|
fc707c7e31 | ||
|
|
eba7e32df1 | ||
|
|
6504e67139 | ||
|
|
428550165a | ||
|
|
a150e77507 | ||
|
|
d1299c11d6 | ||
|
|
fccc9d32ee | ||
|
|
d63c195bff | ||
|
|
e7515584b1 | ||
|
|
0d010968e5 | ||
|
|
5f4fc95c50 | ||
|
|
b2a92ef0bf | ||
|
|
01cdc4a572 | ||
|
|
cdfc881b32 | ||
|
|
d7293aa1cd | ||
|
|
1e78918ad3 | ||
|
|
526b3c4393 | ||
|
|
5d2bfddc15 | ||
|
|
5ac603bbcc | ||
|
|
e41a50af97 | ||
|
|
d3737f5ef7 | ||
|
|
addbd067c8 | ||
|
|
62c842548d | ||
|
|
5526fcac27 | ||
|
|
55c94eb5c0 | ||
|
|
e64d617de6 | ||
|
|
ba456c5eaf | ||
|
|
5e630ffe78 | ||
|
|
9ae75d4840 | ||
|
|
78eeb1219a | ||
|
|
54b2163c56 | ||
|
|
1ca0d2f21d | ||
|
|
2a789f8b01 | ||
|
|
cffba005f0 | ||
|
|
6c72734652 | ||
|
|
83af72a3bc | ||
|
|
ed6285e083 | ||
|
|
57a8c7e652 | ||
|
|
b40a1c54f7 | ||
|
|
8fa4dc75c9 | ||
|
|
99d1521266 | ||
|
|
c78536dfeb | ||
|
|
288dc5be2c | ||
|
|
9ae28e2fab | ||
|
|
f27b66baa3 | ||
|
|
230f0b51f2 | ||
|
|
1f84d0a317 | ||
|
|
2f64d42855 | ||
|
|
9c07a8354e | ||
|
|
a7e9bb7618 | ||
|
|
ad118bbbd6 | ||
|
|
d823382732 | ||
|
|
3a5d9129b2 | ||
|
|
17702d1a76 | ||
|
|
9305bfe190 | ||
|
|
0d30a3793a | ||
|
|
a402c485a3 | ||
|
|
05ad2e5008 | ||
|
|
e3bc22f7d5 | ||
|
|
c703dd6056 | ||
|
|
153de3ffef | ||
|
|
3bcc0db4fc | ||
|
|
142403c6cb | ||
|
|
a2d4554c78 | ||
|
|
a89fd2733b | ||
|
|
39bba05a17 | ||
|
|
a8f27f93b7 | ||
|
|
ce3299f106 | ||
|
|
c568923478 | ||
|
|
54eeb8d563 | ||
|
|
e6a0d2884a | ||
|
|
ceabef7e58 | ||
|
|
0be88ab458 | ||
|
|
30756b12ea | ||
|
|
ec354a8a91 | ||
|
|
1a4b22dff8 | ||
|
|
50a26191ea | ||
|
|
a28b02b0ac | ||
|
|
0829bc641e | ||
|
|
b997773139 | ||
|
|
9044f25f33 | ||
|
|
f8bd224c99 | ||
|
|
4d21f1c19c | ||
|
|
5a3e21788d | ||
|
|
816a97ab47 | ||
|
|
d4509bce5f | ||
|
|
ced0e30506 | ||
|
|
2fb543b144 | ||
|
|
b08f6eefe8 | ||
|
|
65d1881f12 | ||
|
|
b000491514 | ||
|
|
70c4585b88 | ||
|
|
aea3713484 | ||
|
|
7f040011f0 | ||
|
|
c6429eae4f | ||
|
|
d2a30d2801 | ||
|
|
a122be7942 | ||
|
|
61f431dff3 | ||
|
|
b19c83578d | ||
|
|
ddfb18b609 | ||
|
|
095434a4c1 | ||
|
|
8a80ced4b8 | ||
|
|
ef2bb6d510 | ||
|
|
889cfdf47e | ||
|
|
c173449c46 | ||
|
|
c08bae8308 | ||
|
|
80f7b084c0 | ||
|
|
5d24d04adf | ||
|
|
7cc9a70e43 | ||
|
|
5ddfb2c2ca | ||
|
|
242c15bf4c | ||
|
|
867f407401 | ||
|
|
54f49f38f2 | ||
|
|
83d2a782f6 | ||
|
|
d6dd8e0d45 | ||
|
|
9029f8ce34 | ||
|
|
ca23b2ed9a | ||
|
|
664f34f2ac | ||
|
|
1c2c404ca9 | ||
|
|
eef4df9063 | ||
|
|
176da44faf | ||
|
|
619d7d1dfe | ||
|
|
2eab4ec19c | ||
|
|
05eff13ec8 | ||
|
|
4d28266eba | ||
|
|
ba9c50c358 | ||
|
|
e1e5dfccc1 | ||
|
|
1097371cf4 | ||
|
|
0b08d6bc52 | ||
|
|
72ed39a481 | ||
|
|
d4683d941f | ||
|
|
f853108d69 | ||
|
|
045ff10826 | ||
|
|
2a746115ca | ||
|
|
37fddc61d8 | ||
|
|
a050250153 | ||
|
|
42e27a360d | ||
|
|
95b9df187f | ||
|
|
7bbe8f467c | ||
|
|
0c67afb6c3 | ||
|
|
68b7c0d38c | ||
|
|
9ba0e25bfe | ||
|
|
9a64347ea6 | ||
|
|
e4e2b804bc | ||
|
|
4533b3e934 | ||
|
|
fd71773668 | ||
|
|
b17d57f737 | ||
|
|
f5d98e3148 | ||
|
|
5f9536af06 | ||
|
|
7c41b31c37 | ||
|
|
a5c43c26f3 | ||
|
|
82aa1480af | ||
|
|
516be406e0 | ||
|
|
1f4840ba2f | ||
|
|
52bc2cb266 | ||
|
|
18502d5250 | ||
|
|
b3f589df62 | ||
|
|
8ebf1279f9 | ||
|
|
19161ae4a0 | ||
|
|
c24e5c63e8 | ||
|
|
e656570d13 | ||
|
|
9e4209b837 | ||
|
|
ed52123206 | ||
|
|
d10bcd3d6c | ||
|
|
45e81a1b0c | ||
|
|
829707fc5a | ||
|
|
a0d862e1f1 | ||
|
|
1dcc135da5 | ||
|
|
95626abdd3 | ||
|
|
152c5422f1 | ||
|
|
98bfdb322a | ||
|
|
d0d99c31b0 | ||
|
|
0e15d6cea8 | ||
|
|
ed03b1aa7f | ||
|
|
0379fb5614 | ||
|
|
fd1263c9aa | ||
|
|
c0bab015a4 | ||
|
|
4428daa411 | ||
|
|
f821fa0f2d | ||
|
|
2dafef1fab | ||
|
|
d83f7639be | ||
|
|
e08efe2598 | ||
|
|
e4ebca0945 | ||
|
|
6bf3ef47e1 | ||
|
|
7deb0a6db9 | ||
|
|
c106638648 | ||
|
|
4dcbbfba5b | ||
|
|
036c4c8e6f | ||
|
|
9ed4526fee | ||
|
|
b16f12faa3 | ||
|
|
9df02aa335 | ||
|
|
d60d3fe1cb | ||
|
|
a554a588c9 | ||
|
|
4a1842c004 | ||
|
|
39ec208171 | ||
|
|
899de8227d | ||
|
|
5af4a16e57 | ||
|
|
fc07de73e3 | ||
|
|
f5ccc1516b | ||
|
|
de0ec1f739 | ||
|
|
b29f238083 | ||
|
|
2941bb9bb8 | ||
|
|
297b44f24b | ||
|
|
429ed5faa5 | ||
|
|
b3029f75cd | ||
|
|
f7bc3aa77c | ||
|
|
cb77d81f8d | ||
|
|
b87617945e | ||
|
|
9289ce8534 | ||
|
|
798d12b499 | ||
|
|
5146760def | ||
|
|
48649d50b5 | ||
|
|
ccd66419f4 | ||
|
|
80334884fb | ||
|
|
4ab45e8c21 | ||
|
|
c566a7abf3 | ||
|
|
fd0048827d | ||
|
|
74960eaeac | ||
|
|
c49102d688 | ||
|
|
40cf8ba2ce | ||
|
|
ffc0ab2d40 | ||
|
|
7183596586 | ||
|
|
5664bfe4b6 | ||
|
|
4074ff4132 | ||
|
|
e7f9885aa3 | ||
|
|
5641e2ac9b | ||
|
|
4cf951596f | ||
|
|
90efcc1ca7 | ||
|
|
8a1c60e54a | ||
|
|
43665a3892 | ||
|
|
640bdbc066 | ||
|
|
c16d9f78b8 | ||
|
|
91f192ce5b | ||
|
|
e560acdac5 | ||
|
|
9d03178b00 | ||
|
|
041fff5057 | ||
|
|
c7f581daad | ||
|
|
b47168994d | ||
|
|
635b25519b | ||
|
|
bc00c30faf | ||
|
|
a28b2a5b4b | ||
|
|
9e611a6148 | ||
|
|
025091c3fb | ||
|
|
b0cede8231 | ||
|
|
22084b26d4 | ||
|
|
867158a942 | ||
|
|
033bab7db1 | ||
|
|
47d9fad45f | ||
|
|
f82dcbea21 | ||
|
|
67f511b5ad | ||
|
|
010f1a4d2d | ||
|
|
0d4b3ed991 | ||
|
|
2f2c8b57e8 | ||
|
|
b952b103e2 | ||
|
|
c85659ebfc | ||
|
|
73c8577b61 | ||
|
|
84c1a20af7 | ||
|
|
28729657ac | ||
|
|
3ebc132c03 | ||
|
|
11a14543c8 | ||
|
|
7e92f04c93 | ||
|
|
8a74b3e259 | ||
|
|
a9fcd584e9 | ||
|
|
a307618872 | ||
|
|
12749088a0 | ||
|
|
90da233341 | ||
|
|
23226dce8f | ||
|
|
9faed1dad0 | ||
|
|
ea2e45d63f | ||
|
|
5abfe8fca9 | ||
|
|
b339e71973 | ||
|
|
a467f036b1 | ||
|
|
cd5ed011a5 | ||
|
|
9dfdaaf471 | ||
|
|
950d1d072f | ||
|
|
cb14e73c61 | ||
|
|
bd2bd79497 | ||
|
|
62272296da | ||
|
|
1b0fcde862 | ||
|
|
46f39efc43 | ||
|
|
b2ea4a7ce5 | ||
|
|
3e307fe062 | ||
|
|
6ee238d961 | ||
|
|
f3e431912d | ||
|
|
471506c5d4 | ||
|
|
2c05221d89 | ||
|
|
a539cd6939 | ||
|
|
11c3974b0f | ||
|
|
605cf2631e | ||
|
|
ee196fd8a3 | ||
|
|
45b7aa797e | ||
|
|
32dab841d7 | ||
|
|
5b1816719f | ||
|
|
bd2270fb05 | ||
|
|
3f40795a98 | ||
|
|
73bc0f5de7 | ||
|
|
f832c0a4ac | ||
|
|
10579c8834 | ||
|
|
f1a6baadc7 | ||
|
|
5de50b9f91 | ||
|
|
cf0d2679aa | ||
|
|
2d388bf8d0 | ||
|
|
056a86fcae | ||
|
|
ebbfd7c56f | ||
|
|
91d98c4413 | ||
|
|
51b6bb210d | ||
|
|
94634a347d | ||
|
|
e5acd27c9b | ||
|
|
e6cd66df53 | ||
|
|
589e7c72ef | ||
|
|
ecbd44df22 | ||
|
|
4ab2ed8b77 | ||
|
|
eb8dabce84 | ||
|
|
c5df302faa | ||
|
|
a581f1ebcd | ||
|
|
78b27ffedb | ||
|
|
e735bdab60 | ||
|
|
5f1efb6f7e | ||
|
|
c68bfedbaa | ||
|
|
871a185ecb | ||
|
|
ed03841fd1 | ||
|
|
d6c0d53442 | ||
|
|
a2a7d94055 | ||
|
|
2a7e452cf8 | ||
|
|
74ae95038c | ||
|
|
57c364fe87 | ||
|
|
04958ece31 | ||
|
|
838bdd711b | ||
|
|
f644db3c79 | ||
|
|
24e7d07973 | ||
|
|
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 |
9
.azure-pipelines/README.md
Normal file
9
.azure-pipelines/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) Ansible Project
|
||||||
|
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Azure Pipelines Configuration
|
||||||
|
|
||||||
|
Please see the [Documentation](https://github.com/ansible/community/wiki/Testing:-Azure-Pipelines) for more information.
|
||||||
367
.azure-pipelines/azure-pipelines.yml
Normal file
367
.azure-pipelines/azure-pipelines.yml
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
batch: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
|
||||||
|
pr:
|
||||||
|
autoCancel: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
|
||||||
|
schedules:
|
||||||
|
- cron: 0 9 * * *
|
||||||
|
displayName: Nightly
|
||||||
|
always: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
- cron: 0 12 * * 0
|
||||||
|
displayName: Weekly (old stable branches)
|
||||||
|
always: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- stable-*
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: checkoutPath
|
||||||
|
value: ansible_collections/community/crypto
|
||||||
|
- name: coverageBranches
|
||||||
|
value: main
|
||||||
|
- name: entryPoint
|
||||||
|
value: tests/utils/shippable/shippable.sh
|
||||||
|
- name: fetchDepth
|
||||||
|
value: 0
|
||||||
|
|
||||||
|
resources:
|
||||||
|
containers:
|
||||||
|
- container: default
|
||||||
|
image: quay.io/ansible/azure-pipelines-test-container:6.0.0
|
||||||
|
|
||||||
|
pool: Standard
|
||||||
|
|
||||||
|
stages:
|
||||||
|
### Sanity & units
|
||||||
|
- stage: Ansible_devel
|
||||||
|
displayName: Sanity & Units devel
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
targets:
|
||||||
|
- name: Sanity
|
||||||
|
test: 'devel/sanity/1'
|
||||||
|
- name: Sanity Extra # Only on devel
|
||||||
|
test: 'devel/sanity/extra'
|
||||||
|
- name: Units
|
||||||
|
test: 'devel/units/1'
|
||||||
|
- stage: Ansible_2_18
|
||||||
|
displayName: Sanity & Units 2.18
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
targets:
|
||||||
|
- name: Sanity
|
||||||
|
test: '2.18/sanity/1'
|
||||||
|
- name: Units
|
||||||
|
test: '2.18/units/1'
|
||||||
|
- stage: Ansible_2_17
|
||||||
|
displayName: Sanity & Units 2.17
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
targets:
|
||||||
|
- name: Sanity
|
||||||
|
test: '2.17/sanity/1'
|
||||||
|
- name: Units
|
||||||
|
test: '2.17/units/1'
|
||||||
|
- stage: Ansible_2_16
|
||||||
|
displayName: Sanity & Units 2.16
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
targets:
|
||||||
|
- name: Sanity
|
||||||
|
test: '2.16/sanity/1'
|
||||||
|
- name: Units
|
||||||
|
test: '2.16/units/1'
|
||||||
|
### Docker
|
||||||
|
- stage: Docker_devel
|
||||||
|
displayName: Docker devel
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: devel/linux/{0}
|
||||||
|
targets:
|
||||||
|
- name: Fedora 41
|
||||||
|
test: fedora41
|
||||||
|
- name: Ubuntu 24.04
|
||||||
|
test: ubuntu2404
|
||||||
|
- name: Alpine 3.21
|
||||||
|
test: alpine321
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Docker_2_18
|
||||||
|
displayName: Docker 2.18
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.18/linux/{0}
|
||||||
|
targets:
|
||||||
|
- name: Fedora 40
|
||||||
|
test: fedora40
|
||||||
|
- name: Ubuntu 24.04
|
||||||
|
test: ubuntu2404
|
||||||
|
- name: Alpine 3.20
|
||||||
|
test: alpine320
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Docker_2_17
|
||||||
|
displayName: Docker 2.17
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.17/linux/{0}
|
||||||
|
targets:
|
||||||
|
- name: Fedora 39
|
||||||
|
test: fedora39
|
||||||
|
- name: Ubuntu 22.04
|
||||||
|
test: ubuntu2204
|
||||||
|
- name: Alpine 3.19
|
||||||
|
test: alpine319
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Docker_2_16
|
||||||
|
displayName: Docker 2.16
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.16/linux/{0}
|
||||||
|
targets:
|
||||||
|
- name: Fedora 38
|
||||||
|
test: fedora38
|
||||||
|
- name: openSUSE 15
|
||||||
|
test: opensuse15
|
||||||
|
- name: Alpine 3
|
||||||
|
test: alpine3
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
### Community Docker
|
||||||
|
- stage: Docker_community_devel
|
||||||
|
displayName: Docker (community images) devel
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: devel/linux-community/{0}
|
||||||
|
targets:
|
||||||
|
- name: Debian Bullseye
|
||||||
|
test: debian-bullseye/3.9
|
||||||
|
- name: Debian Bookworm
|
||||||
|
test: debian-bookworm/3.11
|
||||||
|
- name: ArchLinux
|
||||||
|
test: archlinux/3.13
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
### Remote
|
||||||
|
- stage: Remote_devel_extra_vms
|
||||||
|
displayName: Remote devel extra VMs
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: devel/{0}
|
||||||
|
targets:
|
||||||
|
- name: Alpine 3.21
|
||||||
|
test: alpine/3.21
|
||||||
|
- name: Fedora 41
|
||||||
|
test: fedora/41
|
||||||
|
- name: Ubuntu 22.04
|
||||||
|
test: ubuntu/22.04
|
||||||
|
- name: Ubuntu 24.04
|
||||||
|
test: ubuntu/24.04
|
||||||
|
groups:
|
||||||
|
- vm
|
||||||
|
- stage: Remote_devel
|
||||||
|
displayName: Remote devel
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: devel/{0}
|
||||||
|
targets:
|
||||||
|
- name: macOS 15.3
|
||||||
|
test: macos/15.3
|
||||||
|
- name: RHEL 9.5
|
||||||
|
test: rhel/9.5
|
||||||
|
- name: FreeBSD 14.2
|
||||||
|
test: freebsd/14.2
|
||||||
|
- name: FreeBSD 13.4
|
||||||
|
test: freebsd/13.4
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Remote_2_18
|
||||||
|
displayName: Remote 2.18
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.18/{0}
|
||||||
|
targets:
|
||||||
|
- name: macOS 14.3
|
||||||
|
test: macos/14.3
|
||||||
|
- name: RHEL 9.4
|
||||||
|
test: rhel/9.4
|
||||||
|
- name: FreeBSD 14.1
|
||||||
|
test: freebsd/14.1
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Remote_2_17
|
||||||
|
displayName: Remote 2.17
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.17/{0}
|
||||||
|
targets:
|
||||||
|
- name: RHEL 9.3
|
||||||
|
test: rhel/9.3
|
||||||
|
- name: FreeBSD 13.3
|
||||||
|
test: freebsd/13.3
|
||||||
|
- name: FreeBSD 14.0
|
||||||
|
test: freebsd/14.0
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Remote_2_16
|
||||||
|
displayName: Remote 2.16
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.16/{0}
|
||||||
|
targets:
|
||||||
|
- name: macOS 13.2
|
||||||
|
test: macos/13.2
|
||||||
|
- name: RHEL 9.2
|
||||||
|
test: rhel/9.2
|
||||||
|
- name: RHEL 8.8
|
||||||
|
test: rhel/8.8
|
||||||
|
- name: RHEL 7.9
|
||||||
|
test: rhel/7.9
|
||||||
|
# - name: FreeBSD 13.2
|
||||||
|
# test: freebsd/13.2
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
### Generic
|
||||||
|
- stage: Generic_devel
|
||||||
|
displayName: Generic devel
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
nameFormat: Python {0}
|
||||||
|
testFormat: devel/generic/{0}
|
||||||
|
targets:
|
||||||
|
- test: "3.8"
|
||||||
|
# - test: "3.9"
|
||||||
|
# - test: "3.10"
|
||||||
|
- test: "3.11"
|
||||||
|
- test: "3.13"
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Generic_2_18
|
||||||
|
displayName: Generic 2.18
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
nameFormat: Python {0}
|
||||||
|
testFormat: 2.18/generic/{0}
|
||||||
|
targets:
|
||||||
|
- test: "3.8"
|
||||||
|
- test: "3.13"
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Generic_2_17
|
||||||
|
displayName: Generic 2.17
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
nameFormat: Python {0}
|
||||||
|
testFormat: 2.17/generic/{0}
|
||||||
|
targets:
|
||||||
|
- test: "3.7"
|
||||||
|
- test: "3.12"
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- stage: Generic_2_16
|
||||||
|
displayName: Generic 2.16
|
||||||
|
dependsOn: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
nameFormat: Python {0}
|
||||||
|
testFormat: 2.16/generic/{0}
|
||||||
|
targets:
|
||||||
|
- test: "2.7"
|
||||||
|
- test: "3.6"
|
||||||
|
- test: "3.11"
|
||||||
|
groups:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
|
||||||
|
## Finally
|
||||||
|
|
||||||
|
- stage: Summary
|
||||||
|
condition: succeededOrFailed()
|
||||||
|
dependsOn:
|
||||||
|
- Ansible_devel
|
||||||
|
- Ansible_2_18
|
||||||
|
- Ansible_2_17
|
||||||
|
- Ansible_2_16
|
||||||
|
- Remote_devel_extra_vms
|
||||||
|
- Remote_devel
|
||||||
|
- Remote_2_18
|
||||||
|
- Remote_2_17
|
||||||
|
- Remote_2_16
|
||||||
|
- Docker_devel
|
||||||
|
- Docker_2_18
|
||||||
|
- Docker_2_17
|
||||||
|
- Docker_2_16
|
||||||
|
- Docker_community_devel
|
||||||
|
- Generic_devel
|
||||||
|
- Generic_2_18
|
||||||
|
- Generic_2_17
|
||||||
|
- Generic_2_16
|
||||||
|
jobs:
|
||||||
|
- template: templates/coverage.yml
|
||||||
28
.azure-pipelines/scripts/aggregate-coverage.sh
Executable file
28
.azure-pipelines/scripts/aggregate-coverage.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Aggregate code coverage results for later processing.
|
||||||
|
|
||||||
|
set -o pipefail -eu
|
||||||
|
|
||||||
|
agent_temp_directory="$1"
|
||||||
|
|
||||||
|
PATH="${PWD}/bin:${PATH}"
|
||||||
|
|
||||||
|
mkdir "${agent_temp_directory}/coverage/"
|
||||||
|
|
||||||
|
if [[ "$(ansible --version)" =~ \ 2\.9\. ]]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
options=(--venv --venv-system-site-packages --color -v)
|
||||||
|
|
||||||
|
ansible-test coverage combine --group-by command --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||||
|
|
||||||
|
if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then
|
||||||
|
# Only analyze coverage if the installed version of ansible-test supports it.
|
||||||
|
# Doing so allows this script to work unmodified for multiple Ansible versions.
|
||||||
|
ansible-test coverage analyze targets generate "${agent_temp_directory}/coverage/coverage-analyze-targets.json" "${options[@]}"
|
||||||
|
fi
|
||||||
64
.azure-pipelines/scripts/combine-coverage.py
Executable file
64
.azure-pipelines/scripts/combine-coverage.py
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
"""
|
||||||
|
Combine coverage data from multiple jobs, keeping the data only from the most recent attempt from each job.
|
||||||
|
Coverage artifacts must be named using the format: "Coverage $(System.JobAttempt) {StableUniqueNameForEachJob}"
|
||||||
|
The recommended coverage artifact name format is: Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)
|
||||||
|
Keep in mind that Azure Pipelines does not enforce unique job display names (only names).
|
||||||
|
It is up to pipeline authors to avoid name collisions when deviating from the recommended format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main program entry point."""
|
||||||
|
source_directory = sys.argv[1]
|
||||||
|
|
||||||
|
if '/ansible_collections/' in os.getcwd():
|
||||||
|
output_path = "tests/output"
|
||||||
|
else:
|
||||||
|
output_path = "test/results"
|
||||||
|
|
||||||
|
destination_directory = os.path.join(output_path, 'coverage')
|
||||||
|
|
||||||
|
if not os.path.exists(destination_directory):
|
||||||
|
os.makedirs(destination_directory)
|
||||||
|
|
||||||
|
jobs = {}
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for name in os.listdir(source_directory):
|
||||||
|
match = re.search('^Coverage (?P<attempt>[0-9]+) (?P<label>.+)$', name)
|
||||||
|
label = match.group('label')
|
||||||
|
attempt = int(match.group('attempt'))
|
||||||
|
jobs[label] = max(attempt, jobs.get(label, 0))
|
||||||
|
|
||||||
|
for label, attempt in jobs.items():
|
||||||
|
name = 'Coverage {attempt} {label}'.format(label=label, attempt=attempt)
|
||||||
|
source = os.path.join(source_directory, name)
|
||||||
|
source_files = os.listdir(source)
|
||||||
|
|
||||||
|
for source_file in source_files:
|
||||||
|
source_path = os.path.join(source, source_file)
|
||||||
|
destination_path = os.path.join(destination_directory, source_file + '.' + label)
|
||||||
|
print('"%s" -> "%s"' % (source_path, destination_path))
|
||||||
|
shutil.copyfile(source_path, destination_path)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print('Coverage file count: %d' % count)
|
||||||
|
print('##vso[task.setVariable variable=coverageFileCount]%d' % count)
|
||||||
|
print('##vso[task.setVariable variable=outputPath]%s' % output_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
28
.azure-pipelines/scripts/process-results.sh
Executable file
28
.azure-pipelines/scripts/process-results.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Check the test results and set variables for use in later steps.
|
||||||
|
|
||||||
|
set -o pipefail -eu
|
||||||
|
|
||||||
|
if [[ "$PWD" =~ /ansible_collections/ ]]; then
|
||||||
|
output_path="tests/output"
|
||||||
|
else
|
||||||
|
output_path="test/results"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "##vso[task.setVariable variable=outputPath]${output_path}"
|
||||||
|
|
||||||
|
if compgen -G "${output_path}"'/junit/*.xml' > /dev/null; then
|
||||||
|
echo "##vso[task.setVariable variable=haveTestResults]true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if compgen -G "${output_path}"'/bot/ansible-test-*' > /dev/null; then
|
||||||
|
echo "##vso[task.setVariable variable=haveBotResults]true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if compgen -G "${output_path}"'/coverage/*' > /dev/null; then
|
||||||
|
echo "##vso[task.setVariable variable=haveCoverageData]true"
|
||||||
|
fi
|
||||||
105
.azure-pipelines/scripts/publish-codecov.py
Executable file
105
.azure-pipelines/scripts/publish-codecov.py
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
"""
|
||||||
|
Upload code coverage reports to codecov.io.
|
||||||
|
Multiple coverage files from multiple languages are accepted and aggregated after upload.
|
||||||
|
Python coverage, as well as PowerShell and Python stubs can all be uploaded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import dataclasses
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import typing as t
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class CoverageFile:
|
||||||
|
name: str
|
||||||
|
path: pathlib.Path
|
||||||
|
flags: t.List[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Args:
|
||||||
|
dry_run: bool
|
||||||
|
path: pathlib.Path
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> Args:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-n', '--dry-run', action='store_true')
|
||||||
|
parser.add_argument('path', type=pathlib.Path)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Store arguments in a typed dataclass
|
||||||
|
fields = dataclasses.fields(Args)
|
||||||
|
kwargs = {field.name: getattr(args, field.name) for field in fields}
|
||||||
|
|
||||||
|
return Args(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
|
||||||
|
processed = []
|
||||||
|
for file in directory.joinpath('reports').glob('coverage*.xml'):
|
||||||
|
name = file.stem.replace('coverage=', '')
|
||||||
|
|
||||||
|
# Get flags from name
|
||||||
|
flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix
|
||||||
|
flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files
|
||||||
|
|
||||||
|
processed.append(CoverageFile(name, file, flags))
|
||||||
|
|
||||||
|
return tuple(processed)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], dry_run: bool = False) -> None:
|
||||||
|
for file in files:
|
||||||
|
cmd = [
|
||||||
|
str(codecov_bin),
|
||||||
|
'--name', file.name,
|
||||||
|
'--file', str(file.path),
|
||||||
|
]
|
||||||
|
for flag in file.flags:
|
||||||
|
cmd.extend(['--flags', flag])
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f'DRY-RUN: Would run command: {cmd}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
|
||||||
|
if dry_run:
|
||||||
|
print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}')
|
||||||
|
return
|
||||||
|
|
||||||
|
with urllib.request.urlopen(url) as resp:
|
||||||
|
with dest.open('w+b') as f:
|
||||||
|
# Read data in chunks rather than all at once
|
||||||
|
shutil.copyfileobj(resp, f, 64 * 1024)
|
||||||
|
|
||||||
|
dest.chmod(flags)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov'
|
||||||
|
with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir:
|
||||||
|
codecov_bin = pathlib.Path(tmpdir) / 'codecov'
|
||||||
|
download_file(url, codecov_bin, 0o755, args.dry_run)
|
||||||
|
|
||||||
|
files = process_files(args.path)
|
||||||
|
upload_files(codecov_bin, files, args.dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
23
.azure-pipelines/scripts/report-coverage.sh
Executable file
23
.azure-pipelines/scripts/report-coverage.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Generate code coverage reports for uploading to Azure Pipelines and codecov.io.
|
||||||
|
|
||||||
|
set -o pipefail -eu
|
||||||
|
|
||||||
|
PATH="${PWD}/bin:${PATH}"
|
||||||
|
|
||||||
|
if [[ "$(ansible --version)" =~ \ 2\.9\. ]]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ansible-test --help >/dev/null 2>&1; then
|
||||||
|
# Install the devel version of ansible-test for generating code coverage reports.
|
||||||
|
# This is only used by Ansible Collections, which are typically tested against multiple Ansible versions (in separate jobs).
|
||||||
|
# Since a version of ansible-test is required that can work the output from multiple older releases, the devel version is used.
|
||||||
|
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
|
||||||
|
fi
|
||||||
|
|
||||||
|
ansible-test coverage xml --group-by command --stub --venv --venv-system-site-packages --color -v
|
||||||
38
.azure-pipelines/scripts/run-tests.sh
Executable file
38
.azure-pipelines/scripts/run-tests.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Configure the test environment and run the tests.
|
||||||
|
|
||||||
|
set -o pipefail -eu
|
||||||
|
|
||||||
|
entry_point="$1"
|
||||||
|
test="$2"
|
||||||
|
read -r -a coverage_branches <<< "$3" # space separated list of branches to run code coverage on for scheduled builds
|
||||||
|
|
||||||
|
export COMMIT_MESSAGE
|
||||||
|
export COMPLETE
|
||||||
|
export COVERAGE
|
||||||
|
export IS_PULL_REQUEST
|
||||||
|
|
||||||
|
if [ "${SYSTEM_PULLREQUEST_TARGETBRANCH:-}" ]; then
|
||||||
|
IS_PULL_REQUEST=true
|
||||||
|
COMMIT_MESSAGE=$(git log --format=%B -n 1 HEAD^2)
|
||||||
|
else
|
||||||
|
IS_PULL_REQUEST=
|
||||||
|
COMMIT_MESSAGE=$(git log --format=%B -n 1 HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMPLETE=
|
||||||
|
COVERAGE=
|
||||||
|
|
||||||
|
if [ "${BUILD_REASON}" = "Schedule" ]; then
|
||||||
|
COMPLETE=yes
|
||||||
|
|
||||||
|
if printf '%s\n' "${coverage_branches[@]}" | grep -q "^${BUILD_SOURCEBRANCHNAME}$"; then
|
||||||
|
COVERAGE=yes
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${entry_point}" "${test}" 2>&1 | "$(dirname "$0")/time-command.py"
|
||||||
29
.azure-pipelines/scripts/time-command.py
Executable file
29
.azure-pipelines/scripts/time-command.py
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
"""Prepends a relative timestamp to each input line from stdin and writes it to stdout."""
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main program entry point."""
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
sys.stdin.reconfigure(errors='surrogateescape')
|
||||||
|
sys.stdout.reconfigure(errors='surrogateescape')
|
||||||
|
|
||||||
|
for line in sys.stdin:
|
||||||
|
seconds = time.time() - start
|
||||||
|
sys.stdout.write('%02d:%02d %s' % (seconds // 60, seconds % 60, line))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
34
.azure-pipelines/templates/coverage.yml
Normal file
34
.azure-pipelines/templates/coverage.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# This template adds a job for processing code coverage data.
|
||||||
|
# It will upload results to Azure Pipelines and codecov.io.
|
||||||
|
# Use it from a job stage that completes after all other jobs have completed.
|
||||||
|
# This can be done by placing it in a separate summary stage that runs after the test stage(s) have completed.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- job: Coverage
|
||||||
|
displayName: Code Coverage
|
||||||
|
container: default
|
||||||
|
workspace:
|
||||||
|
clean: all
|
||||||
|
steps:
|
||||||
|
- checkout: self
|
||||||
|
fetchDepth: $(fetchDepth)
|
||||||
|
path: $(checkoutPath)
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Coverage Data
|
||||||
|
inputs:
|
||||||
|
path: coverage/
|
||||||
|
patterns: "Coverage */*=coverage.combined"
|
||||||
|
- bash: .azure-pipelines/scripts/combine-coverage.py coverage/
|
||||||
|
displayName: Combine Coverage Data
|
||||||
|
- bash: .azure-pipelines/scripts/report-coverage.sh
|
||||||
|
displayName: Generate Coverage Report
|
||||||
|
condition: gt(variables.coverageFileCount, 0)
|
||||||
|
- bash: .azure-pipelines/scripts/publish-codecov.py "$(outputPath)"
|
||||||
|
displayName: Publish to codecov.io
|
||||||
|
condition: gt(variables.coverageFileCount, 0)
|
||||||
|
continueOnError: true
|
||||||
60
.azure-pipelines/templates/matrix.yml
Normal file
60
.azure-pipelines/templates/matrix.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# This template uses the provided targets and optional groups to generate a matrix which is then passed to the test template.
|
||||||
|
# If this matrix template does not provide the required functionality, consider using the test template directly instead.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
# A required list of dictionaries, one per test target.
|
||||||
|
# Each item in the list must contain a "test" or "name" key.
|
||||||
|
# Both may be provided. If one is omitted, the other will be used.
|
||||||
|
- name: targets
|
||||||
|
type: object
|
||||||
|
|
||||||
|
# An optional list of values which will be used to multiply the targets list into a matrix.
|
||||||
|
# Values can be strings or numbers.
|
||||||
|
- name: groups
|
||||||
|
type: object
|
||||||
|
default: []
|
||||||
|
|
||||||
|
# An optional format string used to generate the job name.
|
||||||
|
# - {0} is the name of an item in the targets list.
|
||||||
|
- name: nameFormat
|
||||||
|
type: string
|
||||||
|
default: "{0}"
|
||||||
|
|
||||||
|
# An optional format string used to generate the test name.
|
||||||
|
# - {0} is the name of an item in the targets list.
|
||||||
|
- name: testFormat
|
||||||
|
type: string
|
||||||
|
default: "{0}"
|
||||||
|
|
||||||
|
# An optional format string used to add the group to the job name.
|
||||||
|
# {0} is the formatted name of an item in the targets list.
|
||||||
|
# {{1}} is the group -- be sure to include the double "{{" and "}}".
|
||||||
|
- name: nameGroupFormat
|
||||||
|
type: string
|
||||||
|
default: "{0} - {{1}}"
|
||||||
|
|
||||||
|
# An optional format string used to add the group to the test name.
|
||||||
|
# {0} is the formatted test of an item in the targets list.
|
||||||
|
# {{1}} is the group -- be sure to include the double "{{" and "}}".
|
||||||
|
- name: testGroupFormat
|
||||||
|
type: string
|
||||||
|
default: "{0}/{{1}}"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- template: test.yml
|
||||||
|
parameters:
|
||||||
|
jobs:
|
||||||
|
- ${{ if eq(length(parameters.groups), 0) }}:
|
||||||
|
- ${{ each target in parameters.targets }}:
|
||||||
|
- name: ${{ format(parameters.nameFormat, coalesce(target.name, target.test)) }}
|
||||||
|
test: ${{ format(parameters.testFormat, coalesce(target.test, target.name)) }}
|
||||||
|
- ${{ if not(eq(length(parameters.groups), 0)) }}:
|
||||||
|
- ${{ each group in parameters.groups }}:
|
||||||
|
- ${{ each target in parameters.targets }}:
|
||||||
|
- name: ${{ format(format(parameters.nameGroupFormat, parameters.nameFormat), coalesce(target.name, target.test), group) }}
|
||||||
|
test: ${{ format(format(parameters.testGroupFormat, parameters.testFormat), coalesce(target.test, target.name), group) }}
|
||||||
50
.azure-pipelines/templates/test.yml
Normal file
50
.azure-pipelines/templates/test.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# This template uses the provided list of jobs to create test one or more test jobs.
|
||||||
|
# It can be used directly if needed, or through the matrix template.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
# A required list of dictionaries, one per test job.
|
||||||
|
# Each item in the list must contain a "job" and "name" key.
|
||||||
|
- name: jobs
|
||||||
|
type: object
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- ${{ each job in parameters.jobs }}:
|
||||||
|
- job: test_${{ replace(replace(replace(job.test, '/', '_'), '.', '_'), '-', '_') }}
|
||||||
|
displayName: ${{ job.name }}
|
||||||
|
container: default
|
||||||
|
workspace:
|
||||||
|
clean: all
|
||||||
|
steps:
|
||||||
|
- checkout: self
|
||||||
|
fetchDepth: $(fetchDepth)
|
||||||
|
path: $(checkoutPath)
|
||||||
|
- bash: .azure-pipelines/scripts/run-tests.sh "$(entryPoint)" "${{ job.test }}" "$(coverageBranches)"
|
||||||
|
displayName: Run Tests
|
||||||
|
- bash: .azure-pipelines/scripts/process-results.sh
|
||||||
|
condition: succeededOrFailed()
|
||||||
|
displayName: Process Results
|
||||||
|
- bash: .azure-pipelines/scripts/aggregate-coverage.sh "$(Agent.TempDirectory)"
|
||||||
|
condition: eq(variables.haveCoverageData, 'true')
|
||||||
|
displayName: Aggregate Coverage Data
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
condition: eq(variables.haveTestResults, 'true')
|
||||||
|
inputs:
|
||||||
|
testResultsFiles: "$(outputPath)/junit/*.xml"
|
||||||
|
displayName: Publish Test Results
|
||||||
|
- task: PublishPipelineArtifact@1
|
||||||
|
condition: eq(variables.haveBotResults, 'true')
|
||||||
|
displayName: Publish Bot Results
|
||||||
|
inputs:
|
||||||
|
targetPath: "$(outputPath)/bot/"
|
||||||
|
artifactName: "Bot $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)"
|
||||||
|
- task: PublishPipelineArtifact@1
|
||||||
|
condition: eq(variables.haveCoverageData, 'true')
|
||||||
|
displayName: Publish Coverage Data
|
||||||
|
inputs:
|
||||||
|
targetPath: "$(Agent.TempDirectory)/coverage/"
|
||||||
|
artifactName: "Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)"
|
||||||
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
groups:
|
||||||
|
ci:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
9
.github/patchback.yml
vendored
Normal file
9
.github/patchback.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
backport_branch_prefix: patchback/backports/
|
||||||
|
backport_label_prefix: backport-
|
||||||
|
target_branch_prefix: stable-
|
||||||
|
...
|
||||||
311
.github/workflows/ansible-test.yml
vendored
Normal file
311
.github/workflows/ansible-test.yml
vendored
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# For the comprehensive list of the inputs supported by the ansible-community/ansible-test-gh-action GitHub Action, see
|
||||||
|
# https://github.com/marketplace/actions/ansible-test
|
||||||
|
|
||||||
|
name: EOL CI
|
||||||
|
on:
|
||||||
|
# Run EOL CI against all pushes (direct commits, also merged PRs), Pull Requests
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
pull_request:
|
||||||
|
# Run EOL CI once per day (at 09:00 UTC)
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# Make sure there is at most one active run per PR, but do not cancel any non-PR runs
|
||||||
|
group: ${{ github.workflow }}-${{ (github.head_ref && github.event.number) || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sanity:
|
||||||
|
name: EOL Sanity (Ⓐ${{ matrix.ansible }})
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
ansible:
|
||||||
|
- '2.9'
|
||||||
|
- '2.10'
|
||||||
|
- '2.11'
|
||||||
|
- '2.12'
|
||||||
|
- '2.13'
|
||||||
|
- '2.14'
|
||||||
|
- '2.15'
|
||||||
|
# Ansible-test on various stable branches does not yet work well with cgroups v2.
|
||||||
|
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
|
||||||
|
# image for these stable branches. The list of branches where this is necessary will
|
||||||
|
# shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28
|
||||||
|
# for the latest list.
|
||||||
|
runs-on: >-
|
||||||
|
${{ contains(fromJson(
|
||||||
|
'["2.9", "2.10", "2.11"]'
|
||||||
|
), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }}
|
||||||
|
steps:
|
||||||
|
- name: Perform sanity testing
|
||||||
|
uses: felixfontein/ansible-test-gh-action@main
|
||||||
|
with:
|
||||||
|
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.9", "2.10", "2.11"]'), matrix.ansible) && 'ansible-community/eol-ansible' || 'ansible/ansible' }}
|
||||||
|
ansible-core-version: stable-${{ matrix.ansible }}
|
||||||
|
codecov-token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
|
||||||
|
pull-request-change-detection: 'true'
|
||||||
|
testing-type: sanity
|
||||||
|
|
||||||
|
units:
|
||||||
|
# Ansible-test on various stable branches does not yet work well with cgroups v2.
|
||||||
|
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
|
||||||
|
# image for these stable branches. The list of branches where this is necessary will
|
||||||
|
# shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28
|
||||||
|
# for the latest list.
|
||||||
|
runs-on: >-
|
||||||
|
${{ contains(fromJson(
|
||||||
|
'["2.9", "2.10", "2.11"]'
|
||||||
|
), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }}
|
||||||
|
name: EOL Units (Ⓐ${{ matrix.ansible }})
|
||||||
|
strategy:
|
||||||
|
# As soon as the first unit test fails, cancel the others to free up the CI queue
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
ansible:
|
||||||
|
- '2.9'
|
||||||
|
- '2.10'
|
||||||
|
- '2.11'
|
||||||
|
- '2.12'
|
||||||
|
- '2.13'
|
||||||
|
- '2.14'
|
||||||
|
- '2.15'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: >-
|
||||||
|
Perform unit testing against
|
||||||
|
Ansible version ${{ matrix.ansible }}
|
||||||
|
uses: felixfontein/ansible-test-gh-action@main
|
||||||
|
with:
|
||||||
|
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.9", "2.10", "2.11"]'), matrix.ansible) && 'ansible-community/eol-ansible' || 'ansible/ansible' }}
|
||||||
|
ansible-core-version: stable-${{ matrix.ansible }}
|
||||||
|
codecov-token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
|
||||||
|
pull-request-change-detection: 'true'
|
||||||
|
testing-type: units
|
||||||
|
|
||||||
|
integration:
|
||||||
|
# Ansible-test on various stable branches does not yet work well with cgroups v2.
|
||||||
|
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
|
||||||
|
# image for these stable branches. The list of branches where this is necessary will
|
||||||
|
# shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28
|
||||||
|
# for the latest list.
|
||||||
|
runs-on: >-
|
||||||
|
${{ contains(fromJson(
|
||||||
|
'["2.9", "2.10", "2.11"]'
|
||||||
|
), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }}
|
||||||
|
name: EOL I (Ⓐ${{ matrix.ansible }}+${{ matrix.docker }}+py${{ matrix.python }}:${{ matrix.target }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
ansible:
|
||||||
|
- ''
|
||||||
|
docker:
|
||||||
|
- ''
|
||||||
|
python:
|
||||||
|
- ''
|
||||||
|
target:
|
||||||
|
- ''
|
||||||
|
exclude:
|
||||||
|
- ansible: ''
|
||||||
|
include:
|
||||||
|
# 2.9
|
||||||
|
- ansible: '2.9'
|
||||||
|
docker: ubuntu1804
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.9'
|
||||||
|
docker: ubuntu1804
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.9'
|
||||||
|
docker: default
|
||||||
|
python: '2.7'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.9'
|
||||||
|
docker: default
|
||||||
|
python: '2.7'
|
||||||
|
target: azp/generic/2/
|
||||||
|
# 2.10
|
||||||
|
- ansible: '2.10'
|
||||||
|
docker: centos6
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.10'
|
||||||
|
docker: centos6
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.10'
|
||||||
|
docker: default
|
||||||
|
python: '3.6'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.10'
|
||||||
|
docker: default
|
||||||
|
python: '3.6'
|
||||||
|
target: azp/generic/2/
|
||||||
|
# 2.11
|
||||||
|
- ansible: '2.11'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.11'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.11'
|
||||||
|
docker: default
|
||||||
|
python: '3.8'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.11'
|
||||||
|
docker: default
|
||||||
|
python: '3.8'
|
||||||
|
target: azp/generic/2/
|
||||||
|
# 2.12
|
||||||
|
- ansible: '2.12'
|
||||||
|
docker: centos6
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.12'
|
||||||
|
docker: centos6
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.12'
|
||||||
|
docker: fedora33
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.12'
|
||||||
|
docker: fedora33
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.12'
|
||||||
|
docker: default
|
||||||
|
python: '2.6'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.12'
|
||||||
|
docker: default
|
||||||
|
python: '3.9'
|
||||||
|
target: azp/generic/2/
|
||||||
|
# 2.13
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: opensuse15py2
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: opensuse15py2
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: fedora35
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: fedora35
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: fedora34
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: fedora34
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: ubuntu1804
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: ubuntu1804
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: default
|
||||||
|
python: '3.8'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.13'
|
||||||
|
docker: default
|
||||||
|
python: '3.8'
|
||||||
|
target: azp/generic/2/
|
||||||
|
# 2.14
|
||||||
|
- ansible: '2.14'
|
||||||
|
docker: ubuntu2004
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.14'
|
||||||
|
docker: ubuntu2004
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.14'
|
||||||
|
docker: default
|
||||||
|
python: '3.9'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.14'
|
||||||
|
docker: default
|
||||||
|
python: '3.9'
|
||||||
|
target: azp/generic/2/
|
||||||
|
# 2.15
|
||||||
|
- ansible: '2.15'
|
||||||
|
docker: fedora37
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.15'
|
||||||
|
docker: fedora37
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.15'
|
||||||
|
docker: default
|
||||||
|
python: '3.5'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.15'
|
||||||
|
docker: default
|
||||||
|
python: '3.5'
|
||||||
|
target: azp/generic/2/
|
||||||
|
- ansible: '2.15'
|
||||||
|
docker: default
|
||||||
|
python: '3.10'
|
||||||
|
target: azp/generic/1/
|
||||||
|
- ansible: '2.15'
|
||||||
|
docker: default
|
||||||
|
python: '3.10'
|
||||||
|
target: azp/generic/2/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: >-
|
||||||
|
Perform integration testing against
|
||||||
|
Ansible version ${{ matrix.ansible }}
|
||||||
|
under Python ${{ matrix.python }}
|
||||||
|
uses: felixfontein/ansible-test-gh-action@main
|
||||||
|
with:
|
||||||
|
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.9", "2.10", "2.11"]'), matrix.ansible) && 'ansible-community/eol-ansible' || 'ansible/ansible' }}
|
||||||
|
ansible-core-version: stable-${{ matrix.ansible }}
|
||||||
|
codecov-token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
|
||||||
|
docker-image: ${{ matrix.docker }}
|
||||||
|
integration-continue-on-error: 'false'
|
||||||
|
integration-diff: 'false'
|
||||||
|
integration-retry-on-error: 'true'
|
||||||
|
pre-test-cmd: >-
|
||||||
|
git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ../../community/internal_test_tools
|
||||||
|
;
|
||||||
|
git clone --depth=1 --single-branch https://github.com/ansible-collections/community.general.git ../../community/general
|
||||||
|
pull-request-change-detection: 'true'
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
target-python-version: ${{ matrix.python }}
|
||||||
|
testing-type: integration
|
||||||
95
.github/workflows/docs-pr.yml
vendored
Normal file
95
.github/workflows/docs-pr.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: Collection Docs
|
||||||
|
concurrency:
|
||||||
|
group: docs-pr-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
|
||||||
|
env:
|
||||||
|
GHP_BASE_URL: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docs:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
name: Build Ansible Docs
|
||||||
|
uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-pr.yml@main
|
||||||
|
with:
|
||||||
|
collection-name: community.crypto
|
||||||
|
init-lenient: false
|
||||||
|
init-fail-on-error: true
|
||||||
|
squash-hierarchy: true
|
||||||
|
init-project: Community.Crypto Collection
|
||||||
|
init-copyright: Community.Crypto Contributors
|
||||||
|
init-title: Community.Crypto Collection Documentation
|
||||||
|
init-html-short-title: Community.Crypto Collection Docs
|
||||||
|
init-extra-html-theme-options: |
|
||||||
|
documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/
|
||||||
|
render-file-line: '> * `$<status>` [$<path_tail>](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr/${{ github.event.number }}/$<path_tail>)'
|
||||||
|
|
||||||
|
publish-docs-gh-pages:
|
||||||
|
# for now we won't run this on forks
|
||||||
|
if: github.repository == 'ansible-collections/community.crypto'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
needs: [build-docs]
|
||||||
|
name: Publish Ansible Docs
|
||||||
|
uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main
|
||||||
|
with:
|
||||||
|
artifact-name: ${{ needs.build-docs.outputs.artifact-name }}
|
||||||
|
action: ${{ (github.event.action == 'closed' || needs.build-docs.outputs.changed != 'true') && 'teardown' || 'publish' }}
|
||||||
|
publish-gh-pages-branch: true
|
||||||
|
secrets:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
comment:
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-docs, publish-docs-gh-pages]
|
||||||
|
name: PR comments
|
||||||
|
steps:
|
||||||
|
- name: PR comment
|
||||||
|
uses: ansible-community/github-docs-build/actions/ansible-docs-build-comment@main
|
||||||
|
with:
|
||||||
|
body-includes: '## Docs Build'
|
||||||
|
reactions: heart
|
||||||
|
action: ${{ needs.build-docs.outputs.changed != 'true' && 'remove' || '' }}
|
||||||
|
on-closed-body: |
|
||||||
|
## Docs Build 📝
|
||||||
|
|
||||||
|
This PR is closed and any previously published docsite has been unpublished.
|
||||||
|
on-merged-body: |
|
||||||
|
## Docs Build 📝
|
||||||
|
|
||||||
|
Thank you for contribution!✨
|
||||||
|
|
||||||
|
This PR has been merged and the docs are now incorporated into `main`:
|
||||||
|
${{ env.GHP_BASE_URL }}/branch/main
|
||||||
|
body: |
|
||||||
|
## Docs Build 📝
|
||||||
|
|
||||||
|
Thank you for contribution!✨
|
||||||
|
|
||||||
|
The docs for **this PR** have been published here:
|
||||||
|
${{ env.GHP_BASE_URL }}/pr/${{ github.event.number }}
|
||||||
|
|
||||||
|
You can compare to the docs for the `main` branch here:
|
||||||
|
${{ env.GHP_BASE_URL }}/branch/main
|
||||||
|
|
||||||
|
The docsite for **this PR** is also available for download as an artifact from this run:
|
||||||
|
${{ needs.build-docs.outputs.artifact-url }}
|
||||||
|
|
||||||
|
File changes:
|
||||||
|
|
||||||
|
${{ needs.build-docs.outputs.diff-files-rendered }}
|
||||||
|
|
||||||
|
${{ needs.build-docs.outputs.diff-rendered }}
|
||||||
55
.github/workflows/docs-push.yml
vendored
Normal file
55
.github/workflows/docs-push.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: Collection Docs
|
||||||
|
concurrency:
|
||||||
|
group: docs-push-${{ github.sha }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
# Run CI once per day (at 09:00 UTC)
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * *'
|
||||||
|
# Allow manual trigger (for newer antsibull-docs, sphinx-ansible-theme, ... versions)
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docs:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
name: Build Ansible Docs
|
||||||
|
uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-push.yml@main
|
||||||
|
with:
|
||||||
|
collection-name: community.crypto
|
||||||
|
init-lenient: false
|
||||||
|
init-fail-on-error: true
|
||||||
|
squash-hierarchy: true
|
||||||
|
init-project: Community.Crypto Collection
|
||||||
|
init-copyright: Community.Crypto Contributors
|
||||||
|
init-title: Community.Crypto Collection Documentation
|
||||||
|
init-html-short-title: Community.Crypto Collection Docs
|
||||||
|
init-extra-html-theme-options: |
|
||||||
|
documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/
|
||||||
|
|
||||||
|
publish-docs-gh-pages:
|
||||||
|
# for now we won't run this on forks
|
||||||
|
if: github.repository == 'ansible-collections/community.crypto'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
needs: [build-docs]
|
||||||
|
name: Publish Ansible Docs
|
||||||
|
uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main
|
||||||
|
with:
|
||||||
|
artifact-name: ${{ needs.build-docs.outputs.artifact-name }}
|
||||||
|
publish-gh-pages-branch: true
|
||||||
|
secrets:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
180
.github/workflows/ee.yml
vendored
Normal file
180
.github/workflows/ee.yml
vendored
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: execution environment
|
||||||
|
on:
|
||||||
|
# Run CI against all pushes (direct commits, also merged PRs), Pull Requests
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
pull_request:
|
||||||
|
# Run CI once per day (at 04:45 UTC)
|
||||||
|
# This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder
|
||||||
|
schedule:
|
||||||
|
- cron: '45 4 * * *'
|
||||||
|
|
||||||
|
env:
|
||||||
|
NAMESPACE: community
|
||||||
|
COLLECTION_NAME: crypto
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and test EE (${{ matrix.name }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
name:
|
||||||
|
- ''
|
||||||
|
ansible_core:
|
||||||
|
- ''
|
||||||
|
ansible_runner:
|
||||||
|
- ''
|
||||||
|
base_image:
|
||||||
|
- ''
|
||||||
|
pre_base:
|
||||||
|
- ''
|
||||||
|
extra_vars:
|
||||||
|
- ''
|
||||||
|
other_deps:
|
||||||
|
- ''
|
||||||
|
exclude:
|
||||||
|
- ansible_core: ''
|
||||||
|
include:
|
||||||
|
- name: ansible-core devel @ RHEL UBI 9
|
||||||
|
ansible_core: https://github.com/ansible/ansible/archive/devel.tar.gz
|
||||||
|
ansible_runner: ansible-runner
|
||||||
|
other_deps: |2
|
||||||
|
python_interpreter:
|
||||||
|
package_system: python3.11 python3.11-pip python3.11-wheel python3.11-cryptography
|
||||||
|
python_path: "/usr/bin/python3.11"
|
||||||
|
base_image: docker.io/redhat/ubi9:latest
|
||||||
|
pre_base: '"#"'
|
||||||
|
# For some reason ansible-builder will not install EPEL dependencies on RHEL
|
||||||
|
extra_vars: -e has_no_pyopenssl=true
|
||||||
|
- name: ansible-core 2.15 @ Rocky Linux 9
|
||||||
|
ansible_core: https://github.com/ansible/ansible/archive/stable-2.15.tar.gz
|
||||||
|
ansible_runner: ansible-runner
|
||||||
|
base_image: quay.io/rockylinux/rockylinux:9
|
||||||
|
pre_base: RUN dnf install -y epel-release
|
||||||
|
# For some reason ansible-builder will not install EPEL dependencies on Rocky Linux
|
||||||
|
extra_vars: -e has_no_pyopenssl=true
|
||||||
|
- name: ansible-core 2.14 @ CentOS Stream 9
|
||||||
|
ansible_core: https://github.com/ansible/ansible/archive/stable-2.14.tar.gz
|
||||||
|
ansible_runner: ansible-runner
|
||||||
|
base_image: quay.io/centos/centos:stream9
|
||||||
|
pre_base: RUN dnf install -y epel-release epel-next-release
|
||||||
|
# For some reason, PyOpenSSL is **broken** on CentOS Stream 9 / EPEL
|
||||||
|
extra_vars: -e has_no_pyopenssl=true
|
||||||
|
- name: ansible-core 2.13 @ RHEL UBI 8
|
||||||
|
ansible_core: https://github.com/ansible/ansible/archive/stable-2.13.tar.gz
|
||||||
|
ansible_runner: ansible-runner
|
||||||
|
other_deps: |2
|
||||||
|
python_interpreter:
|
||||||
|
package_system: python39 python39-pip python39-wheel python39-cryptography
|
||||||
|
base_image: docker.io/redhat/ubi8:latest
|
||||||
|
pre_base: '"#"'
|
||||||
|
# We don't have PyOpenSSL for Python 3.9
|
||||||
|
extra_vars: -e has_no_pyopenssl=true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install ansible-builder and ansible-navigator
|
||||||
|
run: pip install ansible-builder ansible-navigator
|
||||||
|
|
||||||
|
- name: Verify requirements
|
||||||
|
run: ansible-builder introspect --sanitize .
|
||||||
|
|
||||||
|
- name: Make sure galaxy.yml has version entry
|
||||||
|
run: >-
|
||||||
|
python -c
|
||||||
|
'import yaml ;
|
||||||
|
f = open("galaxy.yml", "rb") ;
|
||||||
|
data = yaml.safe_load(f) ;
|
||||||
|
f.close() ;
|
||||||
|
data["version"] = data.get("version") or "0.0.1" ;
|
||||||
|
f = open("galaxy.yml", "wb") ;
|
||||||
|
f.write(yaml.dump(data).encode("utf-8")) ;
|
||||||
|
f.close() ;
|
||||||
|
'
|
||||||
|
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
|
||||||
|
|
||||||
|
- name: Build collection
|
||||||
|
run: |
|
||||||
|
ansible-galaxy collection build --output-path ../../../
|
||||||
|
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
|
||||||
|
|
||||||
|
- name: Create files for building execution environment
|
||||||
|
run: |
|
||||||
|
COLLECTION_FILENAME="$(ls "${NAMESPACE}-${COLLECTION_NAME}"-*.tar.gz)"
|
||||||
|
|
||||||
|
# EE config
|
||||||
|
cat > execution-environment.yml <<EOF
|
||||||
|
---
|
||||||
|
version: 3
|
||||||
|
dependencies:
|
||||||
|
ansible_core:
|
||||||
|
package_pip: ${{ matrix.ansible_core }}
|
||||||
|
ansible_runner:
|
||||||
|
package_pip: ${{ matrix.ansible_runner }}
|
||||||
|
galaxy: requirements.yml
|
||||||
|
${{ matrix.other_deps }}
|
||||||
|
|
||||||
|
images:
|
||||||
|
base_image:
|
||||||
|
name: ${{ matrix.base_image }}
|
||||||
|
|
||||||
|
additional_build_files:
|
||||||
|
- src: ${COLLECTION_FILENAME}
|
||||||
|
dest: src
|
||||||
|
|
||||||
|
additional_build_steps:
|
||||||
|
prepend_base:
|
||||||
|
- ${{ matrix.pre_base }}
|
||||||
|
EOF
|
||||||
|
echo "::group::execution-environment.yml"
|
||||||
|
cat execution-environment.yml
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
cat > requirements.yml <<EOF
|
||||||
|
---
|
||||||
|
collections:
|
||||||
|
- name: src/${COLLECTION_FILENAME}
|
||||||
|
type: file
|
||||||
|
EOF
|
||||||
|
echo "::group::requirements.yml"
|
||||||
|
cat requirements.yml
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Build image based on ${{ matrix.base_image }}
|
||||||
|
run: |
|
||||||
|
ansible-builder build --verbosity 3 --tag test-ee:latest --container-runtime podman
|
||||||
|
|
||||||
|
- name: Show images
|
||||||
|
run: podman image ls
|
||||||
|
|
||||||
|
- name: Run basic tests
|
||||||
|
run: >
|
||||||
|
ansible-navigator run
|
||||||
|
--mode stdout
|
||||||
|
--container-engine podman
|
||||||
|
--pull-policy never
|
||||||
|
--set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true
|
||||||
|
--execution-environment-image test-ee:latest
|
||||||
|
-v
|
||||||
|
all.yml
|
||||||
|
${{ matrix.extra_vars }}
|
||||||
|
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee
|
||||||
20
.github/workflows/import-galaxy.yml
vendored
Normal file
20
.github/workflows/import-galaxy.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: import-galaxy
|
||||||
|
'on':
|
||||||
|
# Run CI against all pushes (direct commits, also merged PRs) to main, and all Pull Requests
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
import-galaxy:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
name: Test to import built collection artifact with Galaxy importer
|
||||||
|
uses: ansible-community/github-action-test-galaxy-import/.github/workflows/test-galaxy-import.yml@main
|
||||||
38
.github/workflows/reuse.yml
vendored
Normal file
38
.github/workflows/reuse.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: Verify REUSE
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- stable-*
|
||||||
|
# Run CI once per day (at 04:45 UTC)
|
||||||
|
schedule:
|
||||||
|
- cron: '45 4 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Remove some files before checking REUSE compliance
|
||||||
|
run: |
|
||||||
|
rm -f tests/integration/targets/*/files/*.pem
|
||||||
|
rm -f tests/integration/targets/*/files/roots/*.pem
|
||||||
|
|
||||||
|
- name: REUSE Compliance Check
|
||||||
|
uses: fsfe/reuse-action@v5
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# Community.crypt specific things
|
# Community.crypt specific things
|
||||||
/changelogs/.plugin-cache.yaml
|
/changelogs/.plugin-cache.yaml
|
||||||
|
|
||||||
|
|||||||
5
.reuse/dep5
Normal file
5
.reuse/dep5
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
|
||||||
|
Files: changelogs/fragments/*
|
||||||
|
Copyright: Ansible Project
|
||||||
|
License: GPL-3.0-or-later
|
||||||
1641
CHANGELOG.md
Normal file
1641
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
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
|
||||||
1269
CHANGELOG.rst
1269
CHANGELOG.rst
File diff suppressed because it is too large
Load Diff
3
CHANGELOG.rst.license
Normal file
3
CHANGELOG.rst.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
|
||||||
202
LICENSES/Apache-2.0.txt
Normal file
202
LICENSES/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.
|
||||||
8
LICENSES/BSD-2-Clause.txt
Normal file
8
LICENSES/BSD-2-Clause.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.
|
||||||
|
|
||||||
27
LICENSES/BSD-3-Clause.txt
Normal file
27
LICENSES/BSD-3-Clause.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) Individual contributors.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
3. Neither the name of PyCA Cryptography nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
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 OWNER 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.
|
||||||
1
LICENSES/GPL-3.0-or-later.txt
Symbolic link
1
LICENSES/GPL-3.0-or-later.txt
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../COPYING
|
||||||
48
LICENSES/PSF-2.0.txt
Normal file
48
LICENSES/PSF-2.0.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.
|
||||||
137
README.md
137
README.md
@@ -1,12 +1,44 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) Ansible Project
|
||||||
|
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
# Ansible Community Crypto Collection
|
# Ansible Community Crypto Collection
|
||||||
[](https://app.shippable.com/projects/5e66776ca27f990007073a42)
|
|
||||||
|
[](https://docs.ansible.com/ansible/devel/collections/community/crypto/)
|
||||||
|
[](https://dev.azure.com/ansible/community.crypto/_build?definitionId=21)
|
||||||
|
[](https://github.com/ansible-collections/community.crypto/actions)
|
||||||
[](https://codecov.io/gh/ansible-collections/community.crypto)
|
[](https://codecov.io/gh/ansible-collections/community.crypto)
|
||||||
|
|
||||||
Provides modules for [Ansible](https://www.ansible.com/community) for various cryptographic operations.
|
Provides modules for [Ansible](https://www.ansible.com/community) for various cryptographic operations.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
We follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our interactions within this project.
|
||||||
|
|
||||||
|
If you encounter abusive behavior violating the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html), please refer to the [policy violations](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html#policy-violations) section of the Code of Conduct for information on how to raise a complaint.
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
* Join the Ansible forum:
|
||||||
|
* [Get Help](https://forum.ansible.com/c/help/6): get help or help others. Please add appropriate tags if you start new discussions, for example the `crypto` or `acme` tags.
|
||||||
|
* [Posts tagged with 'crypto'](https://forum.ansible.com/tag/crypto): subscribe to participate in cryptography related conversations.
|
||||||
|
* [Posts tagged with 'acme'](https://forum.ansible.com/tag/acme): subscribe to participate in ACME (RFC 8555) related conversations.
|
||||||
|
* [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts.
|
||||||
|
* [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events.
|
||||||
|
|
||||||
|
* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes.
|
||||||
|
|
||||||
|
For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html).
|
||||||
|
|
||||||
## Tested with Ansible
|
## Tested with Ansible
|
||||||
|
|
||||||
Tested with 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, ansible-core 2.13, ansible-core 2.14, ansible-core 2.15, ansible-core 2.16, ansible-core-2.17, and ansible-core 2.18 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||||
|
|
||||||
## External requirements
|
## External requirements
|
||||||
|
|
||||||
@@ -14,39 +46,68 @@ The exact requirements for every module are listed in the module documentation.
|
|||||||
|
|
||||||
Most modules require a recent enough version of [the Python cryptography library](https://pypi.org/project/cryptography/). See the module documentations for the minimal version supported for each module.
|
Most modules require a recent enough version of [the Python cryptography library](https://pypi.org/project/cryptography/). See the module documentations for the minimal version supported for each module.
|
||||||
|
|
||||||
|
## Collection Documentation
|
||||||
|
|
||||||
|
Browsing the [**latest** collection documentation](https://docs.ansible.com/ansible/latest/collections/community/crypto) will show docs for the _latest version released in the Ansible package_, not the latest version of the collection released on Galaxy.
|
||||||
|
|
||||||
|
Browsing the [**devel** collection documentation](https://docs.ansible.com/ansible/devel/collections/community/crypto) shows docs for the _latest version released on Galaxy_.
|
||||||
|
|
||||||
|
We also separately publish [**latest commit** collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/) which shows docs for the _latest commit in the `main` branch_.
|
||||||
|
|
||||||
|
If you use the Ansible package and do not update collections independently, use **latest**. If you install or update this collection directly from Galaxy, use **devel**. If you are looking to contribute, use **latest commit**.
|
||||||
|
|
||||||
## Included content
|
## Included content
|
||||||
|
|
||||||
- OpenSSL / PKI modules:
|
- OpenSSL / PKI modules and plugins:
|
||||||
- openssl_csr_info
|
- certificate_complete_chain module
|
||||||
- openssl_csr
|
- openssl_csr_info module and filter
|
||||||
- openssl_dhparam
|
- openssl_csr_pipe module
|
||||||
- openssl_pkcs12
|
- openssl_csr module
|
||||||
- openssl_privatekey_info
|
- openssl_dhparam module
|
||||||
- openssl_privatekey
|
- openssl_pkcs12 module
|
||||||
- openssl_publickey
|
- openssl_privatekey_convert module
|
||||||
- openssl_signature_info
|
- openssl_privatekey_info module and filter
|
||||||
- openssl_signature
|
- openssl_privatekey_pipe module
|
||||||
- x509_certificate_info
|
- openssl_privatekey module
|
||||||
- x509_certificate
|
- openssl_publickey_info module and filter
|
||||||
- x509_crl_info
|
- openssl_publickey module
|
||||||
- x509_crl
|
- openssl_signature_info module
|
||||||
- certificate_complete_chain
|
- openssl_signature module
|
||||||
- OpenSSH modules:
|
- split_pem filter
|
||||||
- openssh_cert
|
- x509_certificate_convert module
|
||||||
- openssh_keypair
|
- x509_certificate_info module and filter
|
||||||
- ACME modules:
|
- x509_certificate_pipe module
|
||||||
- acme_account_info
|
- x509_certificate module
|
||||||
- acme_account
|
- x509_crl_info module and filter
|
||||||
- acme_certificate
|
- x509_crl module
|
||||||
- acme_certificate_revoke
|
- OpenSSH modules and plugins:
|
||||||
- acme_challenge_cert_helper
|
- openssh_cert module
|
||||||
- acme_inspect
|
- openssh_keypair module
|
||||||
- ECS modules:
|
- ACME modules and plugins:
|
||||||
- ecs_certificate
|
- acme_account_info module
|
||||||
- ecs_domain
|
- acme_account module
|
||||||
- Miscellaneous modules:
|
- acme_ari_info module
|
||||||
- get_certificate
|
- acme_certificate module
|
||||||
- luks_device
|
- acme_certificate_deactivate_authz module
|
||||||
|
- acme_certificate_order_create module
|
||||||
|
- acme_certificate_order_finalize module
|
||||||
|
- acme_certificate_order_info module
|
||||||
|
- acme_certificate_order_validate module
|
||||||
|
- acme_certificate_revoke module
|
||||||
|
- acme_challenge_cert_helper module
|
||||||
|
- acme_inspect module
|
||||||
|
- ECS modules and plugins:
|
||||||
|
- ecs_certificate module
|
||||||
|
- ecs_domain module
|
||||||
|
- GnuPG modules and plugins:
|
||||||
|
- gpg_fingerprint lookup and filter
|
||||||
|
- Miscellaneous modules and plugins:
|
||||||
|
- crypto_info module
|
||||||
|
- get_certificate module
|
||||||
|
- luks_device module
|
||||||
|
- parse_serial and to_serial filters
|
||||||
|
|
||||||
|
You can also find a list of all modules and plugins with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/), or the [latest commit collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/).
|
||||||
|
|
||||||
## Using this collection
|
## Using this collection
|
||||||
|
|
||||||
@@ -78,7 +139,7 @@ See [Ansible's dev guide](https://docs.ansible.com/ansible/devel/dev_guide/devel
|
|||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
|
||||||
See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/CHANGELOG.rst).
|
See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/CHANGELOG.md).
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -101,6 +162,10 @@ In 2.0.0, the following notable features will be removed:
|
|||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
GNU General Public License v3.0 or later.
|
This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later.
|
||||||
|
|
||||||
See [COPYING](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text.
|
See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.crypto/blob/main/COPYING) for the full text.
|
||||||
|
|
||||||
|
Parts of the collection are licensed under the [Apache 2.0 license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/Apache-2.0.txt) (`plugins/module_utils/crypto/_obj2txt.py` and `plugins/module_utils/crypto/_objects_data.py`), the [BSD 2-Clause license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/BSD-2-Clause.txt) (`plugins/module_utils/ecs/api.py`), the [BSD 3-Clause license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/BSD-3-Clause.txt) (`plugins/module_utils/crypto/_obj2txt.py`, `tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py`), and the [PSF 2.0 license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/PSF-2.0.txt) (`plugins/module_utils/_version.py`). This only applies to vendored files in ``plugins/module_utils/`` and to the ECS module utils.
|
||||||
|
|
||||||
|
Almost all files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `.reuse/dep5`. Right now a few vendored PEM files do not have licensing information as well. This conforms to the [REUSE specification](https://reuse.software/spec/) up to the aforementioned PEM files.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3
changelogs/changelog.yaml.license
Normal file
3
changelogs/changelog.yaml.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
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
changelog_filename_template: ../CHANGELOG.rst
|
changelog_filename_template: ../CHANGELOG.rst
|
||||||
changelog_filename_version_depth: 0
|
changelog_filename_version_depth: 0
|
||||||
changes_file: changelog.yaml
|
changes_file: changelog.yaml
|
||||||
@@ -6,23 +11,31 @@ keep_fragments: false
|
|||||||
mention_ancestor: true
|
mention_ancestor: true
|
||||||
new_plugins_after_name: removed_features
|
new_plugins_after_name: removed_features
|
||||||
notesdir: fragments
|
notesdir: fragments
|
||||||
|
output_formats:
|
||||||
|
- md
|
||||||
|
- rst
|
||||||
prelude_section_name: release_summary
|
prelude_section_name: release_summary
|
||||||
prelude_section_title: Release Summary
|
prelude_section_title: Release Summary
|
||||||
sections:
|
sections:
|
||||||
- - major_changes
|
- - major_changes
|
||||||
- Major Changes
|
- Major Changes
|
||||||
- - minor_changes
|
- - minor_changes
|
||||||
- Minor Changes
|
- Minor Changes
|
||||||
- - breaking_changes
|
- - breaking_changes
|
||||||
- Breaking Changes / Porting Guide
|
- Breaking Changes / Porting Guide
|
||||||
- - deprecated_features
|
- - deprecated_features
|
||||||
- Deprecated Features
|
- Deprecated Features
|
||||||
- - removed_features
|
- - removed_features
|
||||||
- Removed Features (previously deprecated)
|
- Removed Features (previously deprecated)
|
||||||
- - security_fixes
|
- - security_fixes
|
||||||
- Security Fixes
|
- Security Fixes
|
||||||
- - bugfixes
|
- - bugfixes
|
||||||
- Bugfixes
|
- Bugfixes
|
||||||
- - known_issues
|
- - known_issues
|
||||||
- Known Issues
|
- Known Issues
|
||||||
title: Community Crypto
|
title: Community Crypto
|
||||||
|
trivial_section_name: trivial
|
||||||
|
use_fqcn: true
|
||||||
|
add_plugin_period: true
|
||||||
|
changelog_nice_yaml: true
|
||||||
|
changelog_sort: version
|
||||||
|
|||||||
7
docs/docsite/config.yml
Normal file
7
docs/docsite/config.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
write_changelog: true
|
||||||
10
docs/docsite/extra-docs.yml
Normal file
10
docs/docsite/extra-docs.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
sections:
|
||||||
|
- title: Scenario Guides
|
||||||
|
toctree:
|
||||||
|
- guide_selfsigned
|
||||||
|
- guide_ownca
|
||||||
38
docs/docsite/links.yml
Normal file
38
docs/docsite/links.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
edit_on_github:
|
||||||
|
repository: ansible-collections/community.crypto
|
||||||
|
branch: main
|
||||||
|
path_prefix: ''
|
||||||
|
|
||||||
|
extra_links:
|
||||||
|
- description: Ask for help (crypto)
|
||||||
|
url: https://forum.ansible.com/tags/c/help/6/none/crypto
|
||||||
|
- description: Ask for help (ACME)
|
||||||
|
url: https://forum.ansible.com/tags/c/help/6/none/acme
|
||||||
|
- description: Submit a bug report
|
||||||
|
url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=bug_report.md
|
||||||
|
- description: Request a feature
|
||||||
|
url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=feature_request.md
|
||||||
|
|
||||||
|
communication:
|
||||||
|
matrix_rooms:
|
||||||
|
- topic: General usage and support questions
|
||||||
|
room: '#users:ansible.im'
|
||||||
|
irc_channels:
|
||||||
|
- topic: General usage and support questions
|
||||||
|
network: Libera
|
||||||
|
channel: '#ansible'
|
||||||
|
forums:
|
||||||
|
- topic: "Ansible Forum: General usage and support questions"
|
||||||
|
# The following URL directly points to the "Get Help" section
|
||||||
|
url: https://forum.ansible.com/c/help/6/none
|
||||||
|
- topic: "Ansible Forum: Discussions about cryptography"
|
||||||
|
# The following URL directly points to the "crpyto" tag
|
||||||
|
url: https://forum.ansible.com/tag/crpyto
|
||||||
|
- topic: "Ansible Forum: Discussions about ACME (RFC 8555)"
|
||||||
|
# The following URL directly points to the "acme" tag
|
||||||
|
url: https://forum.ansible.com/tag/acme
|
||||||
153
docs/docsite/rst/guide_ownca.rst
Normal file
153
docs/docsite/rst/guide_ownca.rst
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
..
|
||||||
|
Copyright (c) Ansible Project
|
||||||
|
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
.. _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: true
|
||||||
|
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
|
||||||
65
docs/docsite/rst/guide_selfsigned.rst
Normal file
65
docs/docsite/rst/guide_selfsigned.rst
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
..
|
||||||
|
Copyright (c) Ansible Project
|
||||||
|
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
.. _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 :ansopt:`community.crypto.openssl_privatekey#module:path`, the default parameters will be used. This will result in a 4096 bit RSA private key:
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create private key (RSA, 4096 bits)
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /path/to/certificate.key
|
||||||
|
|
||||||
|
You can specify :ansopt:`community.crypto.openssl_privatekey#module:type` to select another key type, :ansopt:`community.crypto.openssl_privatekey#module:size` to select a different key size (only available for RSA and DSA keys), or :ansopt:`community.crypto.openssl_privatekey#module:passphrase` if you want to store the key password-protected:
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- 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 :ansopt:`community.crypto.openssl_privatekey#module:passphrase` for the private key, you have to provide :ansopt:`community.crypto.x509_certificate#module:privatekey_passphrase`.)
|
||||||
|
|
||||||
|
You can use :ansopt:`community.crypto.x509_certificate#module:selfsigned_not_after` to define when the certificate expires (default: in roughly 10 years), and :ansopt:`community.crypto.x509_certificate#module:selfsigned_not_before` to define from when the certificate is valid (default: now).
|
||||||
|
|
||||||
|
To define further properties of the certificate, like the subject, Subject Alternative Names (SANs), key usages, name constraints, etc., you need to first create a Certificate Signing Request (CSR) and provide it to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`. If you do not need the CSR file, you can use the :ref:`community.crypto.openssl_csr_pipe module <ansible_collections.community.crypto.openssl_csr_pipe_module>` as in the example below. (To store it to disk, use the :ref:`community.crypto.openssl_csr module <ansible_collections.community.crypto.openssl_csr_module>` instead.)
|
||||||
|
|
||||||
|
.. 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
|
||||||
19
galaxy.yml
19
galaxy.yml
@@ -1,11 +1,22 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
namespace: community
|
namespace: community
|
||||||
name: crypto
|
name: crypto
|
||||||
version: 1.1.1
|
version: 2.26.0
|
||||||
readme: README.md
|
readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Ansible (github.com/ansible)
|
- Ansible (github.com/ansible)
|
||||||
description: null
|
description: Provides modules and plugins for many cryptographic operations.
|
||||||
license_file: COPYING
|
license:
|
||||||
|
- GPL-3.0-or-later
|
||||||
|
- Apache-2.0
|
||||||
|
- BSD-2-Clause
|
||||||
|
- BSD-3-Clause
|
||||||
|
- PSF-2.0
|
||||||
|
#license_file: COPYING
|
||||||
tags:
|
tags:
|
||||||
- acme
|
- acme
|
||||||
- certificate
|
- certificate
|
||||||
@@ -21,7 +32,7 @@ tags:
|
|||||||
- openssh
|
- openssh
|
||||||
- pkcs12
|
- pkcs12
|
||||||
repository: https://github.com/ansible-collections/community.crypto
|
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
|
homepage: https://github.com/ansible-collections/community.crypto
|
||||||
issues: https://github.com/ansible-collections/community.crypto/issues
|
issues: https://github.com/ansible-collections/community.crypto/issues
|
||||||
build_ignore:
|
build_ignore:
|
||||||
|
|||||||
21
meta/ee-bindep.txt
Normal file
21
meta/ee-bindep.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
cryptsetup [platform:dpkg]
|
||||||
|
cryptsetup [platform:rpm]
|
||||||
|
openssh-client [platform:dpkg]
|
||||||
|
openssh-clients [platform:rpm]
|
||||||
|
openssl [platform:dpkg]
|
||||||
|
openssl [platform:rpm]
|
||||||
|
python3-cryptography [platform:dpkg]
|
||||||
|
python3-cryptography [platform:rpm]
|
||||||
|
python3-openssl [platform:dpkg]
|
||||||
|
# On RHEL 9+, CentOS Stream 9+, and Rocky Linux 9+, python3-pyOpenSSL is part of EPEL
|
||||||
|
python3-pyOpenSSL [platform:rpm !platform:rhel !platform:centos !platform:rocky]
|
||||||
|
python3-pyOpenSSL [platform:rhel-8]
|
||||||
|
python3-pyOpenSSL [platform:rhel !platform:rhel-6 !platform:rhel-7 !platform:rhel-8 epel]
|
||||||
|
python3-pyOpenSSL [platform:centos-8]
|
||||||
|
python3-pyOpenSSL [platform:centos !platform:centos-6 !platform:centos-7 !platform:centos-8 epel]
|
||||||
|
python3-pyOpenSSL [platform:rocky-8]
|
||||||
|
python3-pyOpenSSL [platform:rocky !platform:rocky-8 epel]
|
||||||
5
meta/ee-requirements.txt
Normal file
5
meta/ee-requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
PyYAML
|
||||||
9
meta/execution-environment.yml
Normal file
9
meta/execution-environment.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
dependencies:
|
||||||
|
python: meta/ee-requirements.txt
|
||||||
|
system: meta/ee-bindep.txt
|
||||||
@@ -1,25 +1,39 @@
|
|||||||
|
---
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
requires_ansible: '>=2.9.10'
|
requires_ansible: '>=2.9.10'
|
||||||
|
|
||||||
action_groups:
|
action_groups:
|
||||||
acme:
|
acme:
|
||||||
- acme_inspect
|
- acme_inspect
|
||||||
- acme_certificate_revoke
|
- acme_certificate
|
||||||
- acme_certificate
|
- acme_certificate_deactivate_authz
|
||||||
- acme_account
|
- acme_certificate_order_create
|
||||||
- acme_account_facts
|
- acme_certificate_order_finalize
|
||||||
- acme_account_info
|
- acme_certificate_order_info
|
||||||
|
- acme_certificate_order_validate
|
||||||
|
- acme_certificate_revoke
|
||||||
|
- acme_account
|
||||||
|
- acme_account_info
|
||||||
|
|
||||||
plugin_routing:
|
plugin_routing:
|
||||||
modules:
|
modules:
|
||||||
acme_account_facts:
|
acme_account_facts:
|
||||||
deprecation:
|
tombstone:
|
||||||
removal_version: 2.0.0
|
removal_version: 2.0.0
|
||||||
warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'.
|
warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'.
|
||||||
openssl_certificate:
|
openssl_certificate:
|
||||||
deprecation:
|
tombstone:
|
||||||
removal_version: 2.0.0
|
removal_version: 2.0.0
|
||||||
warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'
|
warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'
|
||||||
openssl_certificate_info:
|
openssl_certificate_info:
|
||||||
deprecation:
|
tombstone:
|
||||||
removal_version: 2.0.0
|
removal_version: 2.0.0
|
||||||
warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'
|
warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'
|
||||||
|
module_utils:
|
||||||
|
crypto.identify:
|
||||||
|
tombstone:
|
||||||
|
removal_version: 2.0.0
|
||||||
|
warning_text: The 'crypto/identify.py' module_utils has been renamed 'crypto/pem.py'. Please update your imports
|
||||||
|
|||||||
128
plugins/action/openssl_privatekey_pipe.py
Normal file
128
plugins/action/openssl_privatekey_pipe.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
self.module.deprecate(
|
||||||
|
'Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0'
|
||||||
|
' to behave the same as without check mode. You can get that behavior right now'
|
||||||
|
' by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this'
|
||||||
|
' breaks your use-case of this module, please create an issue in the'
|
||||||
|
' community.crypto repository',
|
||||||
|
version='3.0.0',
|
||||||
|
collection_name='community.crypto',
|
||||||
|
)
|
||||||
|
self.changed = True
|
||||||
|
elif self.module_backend.needs_conversion():
|
||||||
|
# Convert
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
self.module.deprecate(
|
||||||
|
'Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0'
|
||||||
|
' to behave the same as without check mode. You can get that behavior right now'
|
||||||
|
' by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this'
|
||||||
|
' breaks your use-case of this module, please create an issue in the'
|
||||||
|
' community.crypto repository',
|
||||||
|
version='3.0.0',
|
||||||
|
collection_name='community.crypto',
|
||||||
|
)
|
||||||
|
self.changed = True
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
"""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))
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
@@ -10,108 +11,241 @@ __metaclass__ = type
|
|||||||
class ModuleDocFragment(object):
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
# Standard files documentation fragment
|
# Standard files documentation fragment
|
||||||
DOCUMENTATION = r'''
|
#
|
||||||
|
# NOTE: This document fragment is DEPRECATED and will be removed from community.crypto 3.0.0.
|
||||||
|
# Use both the BASIC and ACCOUNT fragments as a replacement.
|
||||||
|
DOCUMENTATION = r"""
|
||||||
notes:
|
notes:
|
||||||
- "If a new enough version of the C(cryptography) library
|
- If a new enough version of the C(cryptography) library is available (see Requirements for details), it will be used instead
|
||||||
is available (see Requirements for details), it will be used
|
of the C(openssl) binary. This can be explicitly disabled or enabled with the O(select_crypto_backend) option. Note that
|
||||||
instead of the C(openssl) binary. This can be explicitly disabled
|
using the C(openssl) binary will be slower and less secure, as private key contents always have to be stored on disk (see
|
||||||
or enabled with the C(select_crypto_backend) option. Note that using
|
O(account_key_content)).
|
||||||
the C(openssl) binary will be slower and less secure, as private key
|
- Although the defaults are chosen so that the module can be used with the L(Let's Encrypt,https://letsencrypt.org/) CA,
|
||||||
contents always have to be stored on disk (see
|
the module can in principle be used with any CA providing an ACME endpoint, such as L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme).
|
||||||
C(account_key_content))."
|
- So far, the ACME modules have only been tested by the developers against Let's Encrypt (staging and production), Buypass
|
||||||
- "Although the defaults are chosen so that the module can be used with
|
(staging and production), ZeroSSL (production), and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We
|
||||||
the L(Let's Encrypt,https://letsencrypt.org/) CA, the module can in
|
have got community feedback that they also work with Sectigo ACME Service for InCommon. If you experience problems with
|
||||||
principle be used with any CA providing an ACME endpoint, such as
|
another ACME server, please L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
|
||||||
L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)."
|
to help us supporting it. Feedback that an ACME server not mentioned does work is also appreciated.
|
||||||
requirements:
|
requirements:
|
||||||
- python >= 2.6
|
|
||||||
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
|
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
|
||||||
|
- ipaddress
|
||||||
options:
|
options:
|
||||||
account_key_src:
|
account_key_src:
|
||||||
description:
|
description:
|
||||||
- "Path to a file containing the ACME account RSA or Elliptic Curve
|
- Path to a file containing the ACME account RSA or Elliptic Curve key.
|
||||||
key."
|
- 'Private keys can be created with the M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
|
||||||
- "Private keys can be created with the
|
modules. If the requisite (cryptography) is not available, keys can also be created directly with the C(openssl) command
|
||||||
M(community.crypto.openssl_privatekey) module. If the requisites
|
line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can be created with C(openssl ecparam
|
||||||
(pyOpenSSL or cryptography) are not available, keys can also be
|
-genkey ...). Any other tool creating private keys in PEM format can be used as well.'
|
||||||
created directly with the C(openssl) command line tool: RSA keys
|
- Mutually exclusive with O(account_key_content).
|
||||||
can be created with C(openssl genrsa ...). Elliptic curve keys can be
|
- Required if O(account_key_content) is not used.
|
||||||
created with C(openssl ecparam -genkey ...). Any other tool creating
|
|
||||||
private keys in PEM format can be used as well."
|
|
||||||
- "Mutually exclusive with C(account_key_content)."
|
|
||||||
- "Required if C(account_key_content) is not used."
|
|
||||||
type: path
|
type: path
|
||||||
aliases: [ account_key ]
|
aliases: [account_key]
|
||||||
account_key_content:
|
account_key_content:
|
||||||
description:
|
description:
|
||||||
- "Content of the ACME account RSA or Elliptic Curve key."
|
- Content of the ACME account RSA or Elliptic Curve key.
|
||||||
- "Mutually exclusive with C(account_key_src)."
|
- Mutually exclusive with O(account_key_src).
|
||||||
- "Required if C(account_key_src) is not used."
|
- Required if O(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.
|
||||||
be deleted by Ansible when the module completes. Since this is an
|
Since this is an important private key — it can be used to change the account key, or to revoke your certificates
|
||||||
important private key — it can be used to change the account key,
|
without knowing their private keys —, this might not be acceptable.
|
||||||
or to revoke your certificates without knowing their private keys
|
- In case C(cryptography) is used, the content is not written into a temporary file. It can still happen that it is
|
||||||
—, this might not be acceptable."
|
written to disk by Ansible in the process of moving the module with its argument to the node where it is executed.
|
||||||
- "In case C(cryptography) is used, the content is not written into a
|
|
||||||
temporary file. It can still happen that it is written to disk by
|
|
||||||
Ansible in the process of moving the module with its argument to
|
|
||||||
the node where it is executed."
|
|
||||||
type: str
|
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:
|
account_uri:
|
||||||
description:
|
description:
|
||||||
- "If specified, assumes that the account URI is as given. If the
|
- If specified, assumes that the account URI is as given. If the account key does not match this account, or an account
|
||||||
account key does not match this account, or an account with this
|
with this URI does not exist, the module fails.
|
||||||
URI does not exist, the module fails."
|
|
||||||
type: str
|
type: str
|
||||||
acme_version:
|
acme_version:
|
||||||
description:
|
description:
|
||||||
- "The ACME version of the endpoint."
|
- The ACME version of the endpoint.
|
||||||
- "Must be 1 for the classic Let's Encrypt and Buypass ACME endpoints,
|
- Must be V(1) for the classic Let's Encrypt and Buypass ACME endpoints, or V(2) for standardized ACME v2 endpoints.
|
||||||
or 2 for standardized ACME v2 endpoints."
|
- The value V(1) is deprecated since community.crypto 2.0.0 and will be removed from community.crypto 3.0.0.
|
||||||
- "The default value is 1. Note that in Ansible 2.14, this option *will
|
required: true
|
||||||
be required* and will no longer have a default."
|
|
||||||
- "Please also note that we will deprecate ACME v1 support eventually."
|
|
||||||
type: int
|
type: int
|
||||||
choices: [ 1, 2 ]
|
choices: [1, 2]
|
||||||
acme_directory:
|
acme_directory:
|
||||||
description:
|
description:
|
||||||
- "The ACME directory to use. This is the entry point URL to access
|
- The ACME directory to use. This is the entry point URL to access the ACME CA server API.
|
||||||
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
|
||||||
- "For safety reasons the default is set to the Let's Encrypt staging
|
technically correct, but untrusted certificates.
|
||||||
server (for the ACME v1 protocol). This will create technically correct,
|
- "For Let's Encrypt, all staging endpoints can be found here: U(https://letsencrypt.org/docs/staging-environment/).
|
||||||
but untrusted certificates."
|
For Buypass, all endpoints can be found here: U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)."
|
||||||
- "The default value is U(https://acme-staging.api.letsencrypt.org/directory).
|
- For B(Let's Encrypt), the production directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory).
|
||||||
Note that in Ansible 2.14, this option *will be required* and will no longer
|
- For B(Buypass), the production directory URL for ACME v2 and v1 is U(https://api.buypass.com/acme/directory).
|
||||||
have a default."
|
- For B(ZeroSSL), the production directory URL for ACME v2 is U(https://acme.zerossl.com/v2/DV90).
|
||||||
- "For Let's Encrypt, all staging endpoints can be found here:
|
- For B(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV).
|
||||||
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
|
- The notes for this module contain a list of ACME services this module has been tested against.
|
||||||
endpoints can be found here:
|
required: true
|
||||||
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
|
|
||||||
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)."
|
|
||||||
type: str
|
type: str
|
||||||
validate_certs:
|
validate_certs:
|
||||||
description:
|
description:
|
||||||
- Whether calls to the ACME directory will validate TLS certificates.
|
- 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 V(false) for testing purposes, for example when testing against a local
|
||||||
for example when testing against a local Pebble server."
|
Pebble server.
|
||||||
type: bool
|
type: bool
|
||||||
default: yes
|
default: true
|
||||||
select_crypto_backend:
|
select_crypto_backend:
|
||||||
description:
|
description:
|
||||||
- Determines which crypto backend to use.
|
- Determines which crypto backend to use.
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to
|
- The default choice is V(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
|
||||||
C(openssl).
|
- If set to V(openssl), will try to use the C(openssl) binary.
|
||||||
- If set to C(openssl), will try to use the C(openssl) binary.
|
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
- If set to C(cryptography), will try to use the
|
|
||||||
L(cryptography,https://cryptography.io/) library.
|
|
||||||
type: str
|
type: str
|
||||||
default: auto
|
default: auto
|
||||||
choices: [ auto, cryptography, openssl ]
|
choices: [auto, cryptography, openssl]
|
||||||
|
request_timeout:
|
||||||
|
description:
|
||||||
|
- The time Ansible should wait for a response from the ACME API.
|
||||||
|
- This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
|
||||||
|
type: int
|
||||||
|
default: 10
|
||||||
|
version_added: 2.3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Basic documentation fragment without account data
|
||||||
|
BASIC = r"""
|
||||||
|
notes:
|
||||||
|
- Although the defaults are chosen so that the module can be used with the L(Let's Encrypt,https://letsencrypt.org/) CA,
|
||||||
|
the module can in principle be used with any CA providing an ACME endpoint, such as L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme).
|
||||||
|
- So far, the ACME modules have only been tested by the developers against Let's Encrypt (staging and production), Buypass
|
||||||
|
(staging and production), ZeroSSL (production), and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We
|
||||||
|
have got community feedback that they also work with Sectigo ACME Service for InCommon. If you experience problems with
|
||||||
|
another ACME server, please L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
|
||||||
|
to help us supporting it. Feedback that an ACME server not mentioned does work is also appreciated.
|
||||||
|
requirements:
|
||||||
|
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
|
||||||
|
- ipaddress
|
||||||
|
options:
|
||||||
|
acme_version:
|
||||||
|
description:
|
||||||
|
- The ACME version of the endpoint.
|
||||||
|
- Must be V(1) for the classic Let's Encrypt and Buypass ACME endpoints, or V(2) for standardized ACME v2 endpoints.
|
||||||
|
- The value V(1) is deprecated since community.crypto 2.0.0 and will be removed from community.crypto 3.0.0.
|
||||||
|
required: true
|
||||||
|
type: int
|
||||||
|
choices: [1, 2]
|
||||||
|
acme_directory:
|
||||||
|
description:
|
||||||
|
- The ACME directory to use. This is the entry point URL to access the ACME CA server API.
|
||||||
|
- For safety reasons the default is set to the Let's Encrypt staging server (for the ACME v1 protocol). This will create
|
||||||
|
technically correct, but untrusted certificates.
|
||||||
|
- "For Let's Encrypt, all staging endpoints can be found here: U(https://letsencrypt.org/docs/staging-environment/).
|
||||||
|
For Buypass, all endpoints can be found here: U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)."
|
||||||
|
- For B(Let's Encrypt), the production directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory).
|
||||||
|
- For B(Buypass), the production directory URL for ACME v2 and v1 is U(https://api.buypass.com/acme/directory).
|
||||||
|
- For B(ZeroSSL), the production directory URL for ACME v2 is U(https://acme.zerossl.com/v2/DV90).
|
||||||
|
- For B(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV).
|
||||||
|
- The notes for this module contain a list of ACME services this module has been tested against.
|
||||||
|
required: true
|
||||||
|
type: str
|
||||||
|
validate_certs:
|
||||||
|
description:
|
||||||
|
- Whether calls to the ACME directory will validate TLS certificates.
|
||||||
|
- B(Warning:) Should B(only ever) be set to V(false) for testing purposes, for example when testing against a local
|
||||||
|
Pebble server.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- Determines which crypto backend to use.
|
||||||
|
- The default choice is V(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
|
||||||
|
- If set to V(openssl), will try to use the C(openssl) binary.
|
||||||
|
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices: [auto, cryptography, openssl]
|
||||||
|
request_timeout:
|
||||||
|
description:
|
||||||
|
- The time Ansible should wait for a response from the ACME API.
|
||||||
|
- This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
|
||||||
|
type: int
|
||||||
|
default: 10
|
||||||
|
version_added: 2.3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Account data documentation fragment
|
||||||
|
ACCOUNT = r"""
|
||||||
|
notes:
|
||||||
|
- If a new enough version of the C(cryptography) library is available (see Requirements for details), it will be used instead
|
||||||
|
of the C(openssl) binary. This can be explicitly disabled or enabled with the O(select_crypto_backend) option. Note that
|
||||||
|
using the C(openssl) binary will be slower and less secure, as private key contents always have to be stored on disk (see
|
||||||
|
O(account_key_content)).
|
||||||
|
options:
|
||||||
|
account_key_src:
|
||||||
|
description:
|
||||||
|
- Path to a file containing the ACME account RSA or Elliptic Curve key.
|
||||||
|
- 'Private keys can be created with the M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
|
||||||
|
modules. If the requisite (cryptography) is not available, keys can also be created directly with the C(openssl) command
|
||||||
|
line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can be created with C(openssl ecparam
|
||||||
|
-genkey ...). Any other tool creating private keys in PEM format can be used as well.'
|
||||||
|
- Mutually exclusive with O(account_key_content).
|
||||||
|
- Required if O(account_key_content) is not used.
|
||||||
|
type: path
|
||||||
|
aliases: [account_key]
|
||||||
|
account_key_content:
|
||||||
|
description:
|
||||||
|
- Content of the ACME account RSA or Elliptic Curve key.
|
||||||
|
- Mutually exclusive with O(account_key_src).
|
||||||
|
- Required if O(account_key_src) is not used.
|
||||||
|
- B(Warning:) the content will be written into a temporary file, which will be deleted by Ansible when the module completes.
|
||||||
|
Since this is an important private key — it can be used to change the account key, or to revoke your certificates
|
||||||
|
without knowing their private keys —, this might not be acceptable.
|
||||||
|
- In case C(cryptography) is used, the content is not written into a temporary file. It can still happen that it is
|
||||||
|
written to disk by Ansible in the process of moving the module with its argument to the node where it is executed.
|
||||||
|
type: str
|
||||||
|
account_key_passphrase:
|
||||||
|
description:
|
||||||
|
- Phassphrase to use to decode the account key.
|
||||||
|
- B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend.
|
||||||
|
type: str
|
||||||
|
version_added: 1.6.0
|
||||||
|
account_uri:
|
||||||
|
description:
|
||||||
|
- If specified, assumes that the account URI is as given. If the account key does not match this account, or an account
|
||||||
|
with this URI does not exist, the module fails.
|
||||||
|
type: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
# No account data documentation fragment
|
||||||
|
NO_ACCOUNT = r'''
|
||||||
|
notes:
|
||||||
|
- "If a new enough version of the C(cryptography) library
|
||||||
|
is available (see Requirements for details), it will be used
|
||||||
|
instead of the C(openssl) binary. This can be explicitly disabled
|
||||||
|
or enabled with the O(select_crypto_backend) option. Note that using
|
||||||
|
the C(openssl) binary will be slower."
|
||||||
|
options: {}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
CERTIFICATE = r"""
|
||||||
|
options:
|
||||||
|
csr:
|
||||||
|
description:
|
||||||
|
- File containing the CSR for the new certificate.
|
||||||
|
- Can be created with M(community.crypto.openssl_csr).
|
||||||
|
- The CSR may contain multiple Subject Alternate Names, but each one will lead to an individual challenge that must
|
||||||
|
be fulfilled for the CSR to be signed.
|
||||||
|
- 'B(Note): the private key used to create the CSR B(must not) be the account key. This is a bad idea from a security
|
||||||
|
point of view, and the CA should not accept the CSR. The ACME server should return an error in this case.'
|
||||||
|
- Precisely one of O(csr) or O(csr_content) must be specified.
|
||||||
|
type: path
|
||||||
|
csr_content:
|
||||||
|
description:
|
||||||
|
- Content of the CSR for the new certificate.
|
||||||
|
- Can be created with M(community.crypto.openssl_csr_pipe).
|
||||||
|
- The CSR may contain multiple Subject Alternate Names, but each one will lead to an individual challenge that must
|
||||||
|
be fulfilled for the CSR to be signed.
|
||||||
|
- 'B(Note): the private key used to create the CSR B(must not) be the account key. This is a bad idea from a security
|
||||||
|
point of view, and the CA should not accept the CSR. The ACME server should return an error in this case.'
|
||||||
|
- Precisely one of O(csr) or O(csr_content) must be specified.
|
||||||
|
type: str
|
||||||
|
"""
|
||||||
|
|||||||
99
plugins/doc_fragments/attributes.py
Normal file
99
plugins/doc_fragments/attributes.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
|
# Standard documentation fragment
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
check_mode:
|
||||||
|
description: Can run in C(check_mode) and return changed status prediction without modifying target.
|
||||||
|
diff_mode:
|
||||||
|
description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode.
|
||||||
|
idempotent:
|
||||||
|
description:
|
||||||
|
- When run twice in a row outside check mode, with the same arguments, the second invocation indicates no change.
|
||||||
|
- This assumes that the system controlled/queried by the module has not changed in a relevant way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be used together with the standard fragment
|
||||||
|
IDEMPOTENT_NOT_MODIFY_STATE = r"""
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
idempotent:
|
||||||
|
support: full
|
||||||
|
details:
|
||||||
|
- This action does not modify state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be used together with the standard fragment
|
||||||
|
INFO_MODULE = r'''
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
check_mode:
|
||||||
|
support: full
|
||||||
|
details:
|
||||||
|
- This action does not modify state.
|
||||||
|
diff_mode:
|
||||||
|
support: N/A
|
||||||
|
details:
|
||||||
|
- This action does not modify state.
|
||||||
|
'''
|
||||||
|
|
||||||
|
ACTIONGROUP_ACME = r'''
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
action_group:
|
||||||
|
description: Use C(group/acme) or C(group/community.crypto.acme) in C(module_defaults) to set defaults for this module.
|
||||||
|
support: full
|
||||||
|
membership:
|
||||||
|
- community.crypto.acme
|
||||||
|
- acme
|
||||||
|
'''
|
||||||
|
|
||||||
|
FACTS = r"""
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
facts:
|
||||||
|
description: Action returns an C(ansible_facts) dictionary that will update existing host facts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be used together with the standard fragment and the FACTS fragment
|
||||||
|
FACTS_MODULE = r'''
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
check_mode:
|
||||||
|
support: full
|
||||||
|
details:
|
||||||
|
- This action does not modify state.
|
||||||
|
diff_mode:
|
||||||
|
support: N/A
|
||||||
|
details:
|
||||||
|
- This action does not modify state.
|
||||||
|
facts:
|
||||||
|
support: full
|
||||||
|
'''
|
||||||
|
|
||||||
|
FILES = r"""
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
safe_file_operations:
|
||||||
|
description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption.
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLOW = r"""
|
||||||
|
options: {}
|
||||||
|
attributes:
|
||||||
|
action:
|
||||||
|
description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller.
|
||||||
|
async:
|
||||||
|
description: Supports being used with the C(async) keyword.
|
||||||
|
"""
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c), Entrust Datacard Corporation, 2019
|
# Copyright (c), Entrust Datacard Corporation, 2019
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
from __future__ import (absolute_import, division, print_function)
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
@@ -10,34 +11,34 @@ __metaclass__ = type
|
|||||||
class ModuleDocFragment(object):
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
# Plugin options for Entrust Certificate Services (ECS) credentials
|
# Plugin options for Entrust Certificate Services (ECS) credentials
|
||||||
DOCUMENTATION = r'''
|
DOCUMENTATION = r"""
|
||||||
options:
|
options:
|
||||||
entrust_api_user:
|
entrust_api_user:
|
||||||
description:
|
description:
|
||||||
- The username for authentication to the Entrust Certificate Services (ECS) API.
|
- The username for authentication to the Entrust Certificate Services (ECS) API.
|
||||||
type: str
|
type: str
|
||||||
required: true
|
required: true
|
||||||
entrust_api_key:
|
entrust_api_key:
|
||||||
description:
|
description:
|
||||||
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
|
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
|
||||||
type: str
|
type: str
|
||||||
required: true
|
required: true
|
||||||
entrust_api_client_cert_path:
|
entrust_api_client_cert_path:
|
||||||
description:
|
description:
|
||||||
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||||
type: path
|
type: path
|
||||||
required: true
|
required: true
|
||||||
entrust_api_client_cert_key_path:
|
entrust_api_client_cert_key_path:
|
||||||
description:
|
description:
|
||||||
- The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
- The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||||
type: path
|
type: path
|
||||||
required: true
|
required: true
|
||||||
entrust_api_specification_path:
|
entrust_api_specification_path:
|
||||||
description:
|
description:
|
||||||
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
|
- 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.
|
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
|
||||||
type: path
|
type: path
|
||||||
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
|
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
|
||||||
requirements:
|
requirements:
|
||||||
- "PyYAML >= 3.11"
|
- "PyYAML >= 3.11"
|
||||||
'''
|
"""
|
||||||
|
|||||||
411
plugins/doc_fragments/module_certificate.py
Normal file
411
plugins/doc_fragments/module_certificate.py
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
|
# Standard files documentation fragment
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
description:
|
||||||
|
- This module allows one to (re)generate OpenSSL certificates.
|
||||||
|
- It uses the cryptography python library to interact with OpenSSL.
|
||||||
|
attributes:
|
||||||
|
diff_mode:
|
||||||
|
support: full
|
||||||
|
idempotent:
|
||||||
|
support: partial
|
||||||
|
details:
|
||||||
|
- If relative timestamps are used and O(ignore_timestamps=false), the module is not idempotent.
|
||||||
|
- The option O(force=true) generally disables idempotency.
|
||||||
|
requirements:
|
||||||
|
- cryptography >= 1.6 (if using V(selfsigned) or V(ownca) provider)
|
||||||
|
options:
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- Generate the certificate, even if it already exists.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
|
||||||
|
csr_path:
|
||||||
|
description:
|
||||||
|
- Path to the Certificate Signing Request (CSR) used to generate this certificate.
|
||||||
|
- This is mutually exclusive with O(csr_content).
|
||||||
|
type: path
|
||||||
|
csr_content:
|
||||||
|
description:
|
||||||
|
- Content of the Certificate Signing Request (CSR) used to generate this certificate.
|
||||||
|
- This is mutually exclusive with O(csr_path).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
privatekey_path:
|
||||||
|
description:
|
||||||
|
- Path to the private key to use when signing the certificate.
|
||||||
|
- This is mutually exclusive with O(privatekey_content).
|
||||||
|
type: path
|
||||||
|
privatekey_content:
|
||||||
|
description:
|
||||||
|
- Content of the private key to use when signing the certificate.
|
||||||
|
- This is mutually exclusive with O(privatekey_path).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
privatekey_passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the O(privatekey_path) resp. O(privatekey_content).
|
||||||
|
- This is required if the private key is password protected.
|
||||||
|
type: str
|
||||||
|
|
||||||
|
ignore_timestamps:
|
||||||
|
description:
|
||||||
|
- Whether the "not before" and "not after" timestamps should be ignored for idempotency checks.
|
||||||
|
- It is better to keep the default value V(true) when using relative timestamps (like V(+0s) for now).
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
version_added: 2.0.0
|
||||||
|
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- Determines which crypto backend to use.
|
||||||
|
- The default choice is V(auto), which tries to use C(cryptography) if available.
|
||||||
|
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices: [auto, cryptography]
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||||
|
- Date specified should be UTC. Minutes and seconds are mandatory.
|
||||||
|
- For security reason, when you use V(ownca) provider, you should NOT run M(community.crypto.x509_certificate) on a target
|
||||||
|
machine, but on a dedicated CA machine. It is recommended not to store the CA private key on the target machine. Once
|
||||||
|
signed, the certificate can be moved to the target machine.
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.openssl_csr
|
||||||
|
- module: community.crypto.openssl_csr_pipe
|
||||||
|
- module: community.crypto.openssl_dhparam
|
||||||
|
- module: community.crypto.openssl_pkcs12
|
||||||
|
- module: community.crypto.openssl_privatekey
|
||||||
|
- module: community.crypto.openssl_privatekey_pipe
|
||||||
|
- module: community.crypto.openssl_publickey
|
||||||
|
"""
|
||||||
|
|
||||||
|
BACKEND_ACME_DOCUMENTATION = r'''
|
||||||
|
description:
|
||||||
|
- This module allows one to (re)generate OpenSSL certificates.
|
||||||
|
requirements:
|
||||||
|
- acme-tiny >= 4.0.0 (if using the V(acme) provider)
|
||||||
|
options:
|
||||||
|
acme_accountkey_path:
|
||||||
|
description:
|
||||||
|
- The path to the accountkey for the V(acme) provider.
|
||||||
|
- This is only used by the V(acme) provider.
|
||||||
|
type: path
|
||||||
|
|
||||||
|
acme_challenge_path:
|
||||||
|
description:
|
||||||
|
- The path to the ACME challenge directory that is served on U(http://<HOST>:80/.well-known/acme-challenge/)
|
||||||
|
- This is only used by the V(acme) provider.
|
||||||
|
type: path
|
||||||
|
|
||||||
|
acme_chain:
|
||||||
|
description:
|
||||||
|
- Include the intermediate certificate to the generated certificate
|
||||||
|
- This is only used by the V(acme) provider.
|
||||||
|
- Note that this is only available for older versions of C(acme-tiny).
|
||||||
|
New versions include the chain automatically, and setting O(acme_chain) to V(true) results in an error.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
|
||||||
|
acme_directory:
|
||||||
|
description:
|
||||||
|
- "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt."
|
||||||
|
- "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)."
|
||||||
|
type: str
|
||||||
|
default: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
'''
|
||||||
|
|
||||||
|
BACKEND_ENTRUST_DOCUMENTATION = r'''
|
||||||
|
options:
|
||||||
|
entrust_cert_type:
|
||||||
|
description:
|
||||||
|
- Specify the type of certificate requested.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
type: str
|
||||||
|
default: STANDARD_SSL
|
||||||
|
choices: [STANDARD_SSL, ADVANTAGE_SSL, UC_SSL, EV_SSL, WILDCARD_SSL, PRIVATE_SSL, PD_SSL, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT]
|
||||||
|
|
||||||
|
entrust_requester_email:
|
||||||
|
description:
|
||||||
|
- The email of the requester of the certificate (for tracking purposes).
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
entrust_requester_name:
|
||||||
|
description:
|
||||||
|
- The name of the requester of the certificate (for tracking purposes).
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
entrust_requester_phone:
|
||||||
|
description:
|
||||||
|
- The phone number of the requester of the certificate (for tracking purposes).
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
entrust_api_user:
|
||||||
|
description:
|
||||||
|
- The username for authentication to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
entrust_api_key:
|
||||||
|
description:
|
||||||
|
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
entrust_api_client_cert_path:
|
||||||
|
description:
|
||||||
|
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: path
|
||||||
|
|
||||||
|
entrust_api_client_cert_key_path:
|
||||||
|
description:
|
||||||
|
- The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- This is required if the provider is V(entrust).
|
||||||
|
type: path
|
||||||
|
|
||||||
|
entrust_not_after:
|
||||||
|
description:
|
||||||
|
- The point in time at which the certificate stops being valid.
|
||||||
|
- Time can be specified either as relative time or as an absolute timestamp.
|
||||||
|
- A valid absolute time format is C(ASN.1 TIME) such as V(2019-06-18).
|
||||||
|
- A valid relative time format is V([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as V(+365d) or V(+32w1d2h)).
|
||||||
|
- Time will always be interpreted as UTC.
|
||||||
|
- Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate.
|
||||||
|
- The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day
|
||||||
|
earlier than expected if a relative time is used.
|
||||||
|
- The minimum certificate lifetime is 90 days, and maximum is three years.
|
||||||
|
- If this value is not specified, the certificate will stop being valid 365 days the date of issue.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
- Please note that this value is B(not) covered by the O(ignore_timestamps) option.
|
||||||
|
type: str
|
||||||
|
default: +365d
|
||||||
|
|
||||||
|
entrust_api_specification_path:
|
||||||
|
description:
|
||||||
|
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
|
||||||
|
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
|
||||||
|
- This is only used by the V(entrust) provider.
|
||||||
|
type: path
|
||||||
|
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
|
||||||
|
'''
|
||||||
|
|
||||||
|
BACKEND_OWNCA_DOCUMENTATION = r'''
|
||||||
|
description:
|
||||||
|
- The V(ownca) provider is intended for generating an OpenSSL certificate signed with your own
|
||||||
|
CA (Certificate Authority) certificate (self-signed certificate).
|
||||||
|
options:
|
||||||
|
ownca_path:
|
||||||
|
description:
|
||||||
|
- Remote absolute path of the CA (Certificate Authority) certificate.
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
- This is mutually exclusive with O(ownca_content).
|
||||||
|
type: path
|
||||||
|
ownca_content:
|
||||||
|
description:
|
||||||
|
- Content of the CA (Certificate Authority) certificate.
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
- This is mutually exclusive with O(ownca_path).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
ownca_privatekey_path:
|
||||||
|
description:
|
||||||
|
- Path to the CA (Certificate Authority) private key to use when signing the certificate.
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
- This is mutually exclusive with O(ownca_privatekey_content).
|
||||||
|
type: path
|
||||||
|
ownca_privatekey_content:
|
||||||
|
description:
|
||||||
|
- Content of the CA (Certificate Authority) private key to use when signing the certificate.
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
- This is mutually exclusive with O(ownca_privatekey_path).
|
||||||
|
type: str
|
||||||
|
|
||||||
|
ownca_privatekey_passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the O(ownca_privatekey_path) resp. O(ownca_privatekey_content).
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
type: str
|
||||||
|
|
||||||
|
ownca_digest:
|
||||||
|
description:
|
||||||
|
- The digest algorithm to be used for the V(ownca) certificate.
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
type: str
|
||||||
|
default: sha256
|
||||||
|
|
||||||
|
ownca_version:
|
||||||
|
description:
|
||||||
|
- The version of the V(ownca) certificate.
|
||||||
|
- Nowadays it should almost always be V(3).
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
type: int
|
||||||
|
default: 3
|
||||||
|
|
||||||
|
ownca_not_before:
|
||||||
|
description:
|
||||||
|
- The point in time the certificate is valid from.
|
||||||
|
- Time can be specified either as relative time or as absolute timestamp.
|
||||||
|
- Time will always be interpreted as UTC.
|
||||||
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
|
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
|
||||||
|
- If this value is not specified, the certificate will start being valid from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
|
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
|
||||||
|
avoid relative timestamps when setting O(ignore_timestamps=false).
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
type: str
|
||||||
|
default: +0s
|
||||||
|
|
||||||
|
ownca_not_after:
|
||||||
|
description:
|
||||||
|
- The point in time at which the certificate stops being valid.
|
||||||
|
- Time can be specified either as relative time or as absolute timestamp.
|
||||||
|
- Time will always be interpreted as UTC.
|
||||||
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
|
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
|
||||||
|
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
|
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
|
||||||
|
avoid relative timestamps when setting O(ignore_timestamps=false).
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
|
||||||
|
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||||
|
type: str
|
||||||
|
default: +3650d
|
||||||
|
|
||||||
|
ownca_create_subject_key_identifier:
|
||||||
|
description:
|
||||||
|
- Whether to create the Subject Key Identifier (SKI) from the public key.
|
||||||
|
- A value of V(create_if_not_provided) (default) only creates a SKI when the CSR does not
|
||||||
|
provide one.
|
||||||
|
- A value of V(always_create) always creates a SKI. If the CSR provides one, that one is
|
||||||
|
ignored.
|
||||||
|
- A value of V(never_create) never creates a SKI. If the CSR provides one, that one is used.
|
||||||
|
- This is only used by the V(ownca) provider.
|
||||||
|
- 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 V(ownca) provider.
|
||||||
|
- Note that this is only supported if the C(cryptography) backend is used!
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
BACKEND_SELFSIGNED_DOCUMENTATION = r'''
|
||||||
|
notes:
|
||||||
|
- For the V(selfsigned) provider, O(csr_path) and O(csr_content) are optional. If not provided, a
|
||||||
|
certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created.
|
||||||
|
|
||||||
|
options:
|
||||||
|
# NOTE: descriptions in options are overwritten, not appended. For that reason, the texts provided
|
||||||
|
# here for csr_path and csr_content are not visible to the user. That's why this information is
|
||||||
|
# added to the notes (see above).
|
||||||
|
|
||||||
|
# csr_path:
|
||||||
|
# description:
|
||||||
|
# - This is optional for the V(selfsigned) provider. If not provided, a certificate
|
||||||
|
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
|
||||||
|
# created.
|
||||||
|
|
||||||
|
# csr_content:
|
||||||
|
# description:
|
||||||
|
# - This is optional for the V(selfsigned) provider. If not provided, a certificate
|
||||||
|
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
|
||||||
|
# created.
|
||||||
|
|
||||||
|
selfsigned_version:
|
||||||
|
description:
|
||||||
|
- Version of the V(selfsigned) certificate.
|
||||||
|
- Nowadays it should almost always be V(3).
|
||||||
|
- This is only used by the V(selfsigned) provider.
|
||||||
|
type: int
|
||||||
|
default: 3
|
||||||
|
|
||||||
|
selfsigned_digest:
|
||||||
|
description:
|
||||||
|
- Digest algorithm to be used when self-signing the certificate.
|
||||||
|
- This is only used by the V(selfsigned) provider.
|
||||||
|
type: str
|
||||||
|
default: sha256
|
||||||
|
|
||||||
|
selfsigned_not_before:
|
||||||
|
description:
|
||||||
|
- The point in time the certificate is valid from.
|
||||||
|
- Time can be specified either as relative time or as absolute timestamp.
|
||||||
|
- Time will always be interpreted as UTC.
|
||||||
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
|
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
|
||||||
|
- If this value is not specified, the certificate will start being valid from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
|
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
|
||||||
|
avoid relative timestamps when setting O(ignore_timestamps=false).
|
||||||
|
- This is only used by the V(selfsigned) provider.
|
||||||
|
type: str
|
||||||
|
default: +0s
|
||||||
|
aliases: [ selfsigned_notBefore ]
|
||||||
|
|
||||||
|
selfsigned_not_after:
|
||||||
|
description:
|
||||||
|
- The point in time at which the certificate stops being valid.
|
||||||
|
- Time can be specified either as relative time or as absolute timestamp.
|
||||||
|
- Time will always be interpreted as UTC.
|
||||||
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
|
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
|
||||||
|
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
|
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
|
||||||
|
avoid relative timestamps when setting O(ignore_timestamps=false).
|
||||||
|
- This is only used by the V(selfsigned) provider.
|
||||||
|
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
|
||||||
|
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||||
|
type: str
|
||||||
|
default: +3650d
|
||||||
|
aliases: [ selfsigned_notAfter ]
|
||||||
|
|
||||||
|
selfsigned_create_subject_key_identifier:
|
||||||
|
description:
|
||||||
|
- Whether to create the Subject Key Identifier (SKI) from the public key.
|
||||||
|
- A value of V(create_if_not_provided) (default) only creates a SKI when the CSR does not
|
||||||
|
provide one.
|
||||||
|
- A value of V(always_create) always creates a SKI. If the CSR provides one, that one is
|
||||||
|
ignored.
|
||||||
|
- A value of V(never_create) never creates a SKI. If the CSR provides one, that one is used.
|
||||||
|
- This is only used by the V(selfsigned) provider.
|
||||||
|
- 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
|
||||||
|
'''
|
||||||
321
plugins/doc_fragments/module_csr.py
Normal file
321
plugins/doc_fragments/module_csr.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
|
# Standard files documentation fragment
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
description:
|
||||||
|
- This module allows one to (re)generate OpenSSL certificate signing requests.
|
||||||
|
- This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple extensions.
|
||||||
|
attributes:
|
||||||
|
diff_mode:
|
||||||
|
support: full
|
||||||
|
idempotent:
|
||||||
|
support: full
|
||||||
|
requirements:
|
||||||
|
- cryptography >= 1.3
|
||||||
|
options:
|
||||||
|
digest:
|
||||||
|
description:
|
||||||
|
- The digest used when signing the certificate signing request with the private key.
|
||||||
|
type: str
|
||||||
|
default: sha256
|
||||||
|
privatekey_path:
|
||||||
|
description:
|
||||||
|
- The path to the private key to use when signing the certificate signing request.
|
||||||
|
- Either O(privatekey_path) or O(privatekey_content) must be specified if O(state) is V(present), but not both.
|
||||||
|
type: path
|
||||||
|
privatekey_content:
|
||||||
|
description:
|
||||||
|
- The content of the private key to use when signing the certificate signing request.
|
||||||
|
- Either O(privatekey_path) or O(privatekey_content) must be specified if O(state) is V(present), but not both.
|
||||||
|
type: str
|
||||||
|
privatekey_passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the private key.
|
||||||
|
- This is required if the private key is password protected.
|
||||||
|
type: str
|
||||||
|
version:
|
||||||
|
description:
|
||||||
|
- The version of the certificate signing request.
|
||||||
|
- The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1) is 1.
|
||||||
|
- This option no longer accepts unsupported values since community.crypto 2.0.0.
|
||||||
|
type: int
|
||||||
|
default: 1
|
||||||
|
choices:
|
||||||
|
- 1
|
||||||
|
subject:
|
||||||
|
description:
|
||||||
|
- Key/value pairs that will be present in the subject name field of the certificate signing request.
|
||||||
|
- If you need to specify more than one value with the same key, use a list as value.
|
||||||
|
- If the order of the components is important, use O(subject_ordered).
|
||||||
|
- Mutually exclusive with O(subject_ordered).
|
||||||
|
type: dict
|
||||||
|
subject_ordered:
|
||||||
|
description:
|
||||||
|
- A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair will be present
|
||||||
|
in the subject name field of the certificate signing request.
|
||||||
|
- If you want to specify more than one value with the same key in a row, you can use a list as value.
|
||||||
|
- Mutually exclusive with O(subject), and any other subject field option, such as O(country_name), O(state_or_province_name),
|
||||||
|
O(locality_name), O(organization_name), O(organizational_unit_name), O(common_name), or O(email_address).
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
version_added: 2.0.0
|
||||||
|
country_name:
|
||||||
|
description:
|
||||||
|
- The countryName field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [C, countryName]
|
||||||
|
state_or_province_name:
|
||||||
|
description:
|
||||||
|
- The stateOrProvinceName field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [ST, stateOrProvinceName]
|
||||||
|
locality_name:
|
||||||
|
description:
|
||||||
|
- The localityName field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [L, localityName]
|
||||||
|
organization_name:
|
||||||
|
description:
|
||||||
|
- The organizationName field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [O, organizationName]
|
||||||
|
organizational_unit_name:
|
||||||
|
description:
|
||||||
|
- The organizationalUnitName field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [OU, organizationalUnitName]
|
||||||
|
common_name:
|
||||||
|
description:
|
||||||
|
- The commonName field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [CN, commonName]
|
||||||
|
email_address:
|
||||||
|
description:
|
||||||
|
- The emailAddress field of the certificate signing request subject.
|
||||||
|
type: str
|
||||||
|
aliases: [E, emailAddress]
|
||||||
|
subject_alt_name:
|
||||||
|
description:
|
||||||
|
- Subject Alternative Name (SAN) extension to attach to the certificate signing request.
|
||||||
|
- Values must be prefixed by their options. (These are C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
|
||||||
|
and the ones specific to your CA).
|
||||||
|
- Note that if no SAN is specified, but a common name, the common name will be added as a SAN except if O(use_common_name_for_san)
|
||||||
|
is set to V(false).
|
||||||
|
- More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6).
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
aliases: [subjectAltName]
|
||||||
|
subject_alt_name_critical:
|
||||||
|
description:
|
||||||
|
- Should the subjectAltName extension be considered as critical.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
aliases: [subjectAltName_critical]
|
||||||
|
use_common_name_for_san:
|
||||||
|
description:
|
||||||
|
- If set to V(true), the module will fill the common name in for O(subject_alt_name) with C(DNS:) prefix if no SAN is
|
||||||
|
specified.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
aliases: [useCommonNameForSAN]
|
||||||
|
key_usage:
|
||||||
|
description:
|
||||||
|
- This defines the purpose (for example encipherment, signature, certificate signing) of the key contained in the certificate.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
aliases: [keyUsage]
|
||||||
|
key_usage_critical:
|
||||||
|
description:
|
||||||
|
- Should the keyUsage extension be considered as critical.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
aliases: [keyUsage_critical]
|
||||||
|
extended_key_usage:
|
||||||
|
description:
|
||||||
|
- Additional restrictions (for example client authentication, server authentication) on the allowed purposes for which
|
||||||
|
the public key may be used.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
aliases: [extKeyUsage, extendedKeyUsage]
|
||||||
|
extended_key_usage_critical:
|
||||||
|
description:
|
||||||
|
- Should the extkeyUsage extension be considered as critical.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
aliases: [extKeyUsage_critical, extendedKeyUsage_critical]
|
||||||
|
basic_constraints:
|
||||||
|
description:
|
||||||
|
- Indicates basic constraints, such as if the certificate is a CA.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
aliases: [basicConstraints]
|
||||||
|
basic_constraints_critical:
|
||||||
|
description:
|
||||||
|
- Should the basicConstraints extension be considered as critical.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
aliases: [basicConstraints_critical]
|
||||||
|
ocsp_must_staple:
|
||||||
|
description:
|
||||||
|
- Indicates that the certificate should contain the OCSP Must Staple extension (U(https://tools.ietf.org/html/rfc7633)).
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
aliases: [ocspMustStaple]
|
||||||
|
ocsp_must_staple_critical:
|
||||||
|
description:
|
||||||
|
- Should the OCSP Must Staple extension be considered as critical.
|
||||||
|
- Note that according to the RFC, this extension should not be marked as critical, as old clients not knowing about
|
||||||
|
OCSP Must Staple are required to reject such certificates (see U(https://tools.ietf.org/html/rfc7633#section-4)).
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
aliases: [ocspMustStaple_critical]
|
||||||
|
name_constraints_permitted:
|
||||||
|
description:
|
||||||
|
- For CA certificates, this specifies a list of identifiers which describe subtrees of names that this CA is allowed
|
||||||
|
to issue certificates for.
|
||||||
|
- Values must be prefixed by their options. (That is, C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
|
||||||
|
and the ones specific to your CA).
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
name_constraints_excluded:
|
||||||
|
description:
|
||||||
|
- For CA certificates, this specifies a list of identifiers which describe subtrees of names that this CA is B(not)
|
||||||
|
allowed to issue certificates for.
|
||||||
|
- Values must be prefixed by their options. (That is, C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
|
||||||
|
and the ones specific to your CA).
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
name_constraints_critical:
|
||||||
|
description:
|
||||||
|
- Should the Name Constraints extension be considered as critical.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- Determines which crypto backend to use.
|
||||||
|
- The default choice is V(auto), which tries to use C(cryptography) if available.
|
||||||
|
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices: [auto, cryptography]
|
||||||
|
create_subject_key_identifier:
|
||||||
|
description:
|
||||||
|
- Create the Subject Key Identifier from the public key.
|
||||||
|
- Please note that commercial CAs can ignore the value, respectively use a value of their own choice instead. Specifying
|
||||||
|
this option is mostly useful for self-signed certificates or for own CAs.
|
||||||
|
- Note that this is only supported if the C(cryptography) backend is used!
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
subject_key_identifier:
|
||||||
|
description:
|
||||||
|
- The subject key identifier as a hex string, where two bytes are separated by colons.
|
||||||
|
- 'Example: V(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33).'
|
||||||
|
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
|
||||||
|
is mostly useful for self-signed certificates or for own CAs.
|
||||||
|
- Note that this option can only be used if O(create_subject_key_identifier) is V(false).
|
||||||
|
- 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: V(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33).'
|
||||||
|
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
|
||||||
|
is mostly useful for self-signed certificates or for own CAs.
|
||||||
|
- Note that this is only supported if the C(cryptography) backend is used!
|
||||||
|
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of O(authority_key_identifier), O(authority_cert_issuer)
|
||||||
|
and O(authority_cert_serial_number) is specified.
|
||||||
|
type: str
|
||||||
|
authority_cert_issuer:
|
||||||
|
description:
|
||||||
|
- Names that will be present in the authority cert issuer field of the certificate signing request.
|
||||||
|
- Values must be prefixed by their options. (That is, C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
|
||||||
|
and the ones specific to your CA).
|
||||||
|
- 'Example: V(DNS:ca.example.org).'
|
||||||
|
- If specified, O(authority_cert_serial_number) must also be specified.
|
||||||
|
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
|
||||||
|
is mostly useful for self-signed certificates or for own CAs.
|
||||||
|
- 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 O(authority_key_identifier), O(authority_cert_issuer)
|
||||||
|
and O(authority_cert_serial_number) is specified.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
authority_cert_serial_number:
|
||||||
|
description:
|
||||||
|
- The authority cert serial number.
|
||||||
|
- If specified, O(authority_cert_issuer) must also be specified.
|
||||||
|
- 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 O(authority_key_identifier), O(authority_cert_issuer)
|
||||||
|
and O(authority_cert_serial_number) is specified.
|
||||||
|
- This option accepts an B(integer). If you want to provide serial numbers as colon-separated hex strings, such as C(11:22:33),
|
||||||
|
you need to convert them to an integer with P(community.crypto.parse_serial#filter).
|
||||||
|
type: int
|
||||||
|
crl_distribution_points:
|
||||||
|
description:
|
||||||
|
- Allows to specify one or multiple CRL distribution points.
|
||||||
|
- 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 O(crl_distribution_points[].relative_name).
|
||||||
|
- 'Example: V(URI:https://ca.example.com/revocations.crl).'
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
relative_name:
|
||||||
|
description:
|
||||||
|
- Describes how the CRL can be retrieved relative to the CRL issuer.
|
||||||
|
- Mutually exclusive with O(crl_distribution_points[].full_name).
|
||||||
|
- 'Example: V(/CN=example.com).'
|
||||||
|
- 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
|
||||||
|
- plugin: community.crypto.parse_serial
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
147
plugins/doc_fragments/module_privatekey.py
Normal file
147
plugins/doc_fragments/module_privatekey.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
|
# Standard files documentation fragment
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
description:
|
||||||
|
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29), L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
|
||||||
|
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private
|
||||||
|
keys.
|
||||||
|
- Keys are generated in PEM format.
|
||||||
|
attributes:
|
||||||
|
diff_mode:
|
||||||
|
support: full
|
||||||
|
idempotent:
|
||||||
|
support: partial
|
||||||
|
details:
|
||||||
|
- The option O(regenerate=always) generally disables idempotency.
|
||||||
|
requirements:
|
||||||
|
- cryptography >= 1.2.3 (older versions might work as well)
|
||||||
|
options:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Size (in bits) of the TLS/SSL key to generate.
|
||||||
|
type: int
|
||||||
|
default: 4096
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- The algorithm used to generate the TLS/SSL private key.
|
||||||
|
- Note that V(ECC), V(X25519), V(X448), V(Ed25519), and V(Ed448) require the C(cryptography) backend. V(X25519) needs
|
||||||
|
cryptography 2.5 or newer, while V(X448), V(Ed25519), and V(Ed448) require cryptography 2.6 or newer. For V(ECC),
|
||||||
|
the minimal cryptography version required depends on the O(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, V(secp384r1) or V(secp256r1) should be used.
|
||||||
|
- We use the curve names as defined in the L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
|
||||||
|
- Please note that all curves except V(secp224r1), V(secp256k1), V(secp256r1), V(secp384r1), and V(secp521r1) are discouraged
|
||||||
|
for new private keys.
|
||||||
|
type: str
|
||||||
|
choices:
|
||||||
|
- secp224r1
|
||||||
|
- secp256k1
|
||||||
|
- secp256r1
|
||||||
|
- secp384r1
|
||||||
|
- secp521r1
|
||||||
|
- secp192r1
|
||||||
|
- brainpoolP256r1
|
||||||
|
- brainpoolP384r1
|
||||||
|
- brainpoolP512r1
|
||||||
|
- sect163k1
|
||||||
|
- sect163r2
|
||||||
|
- sect233k1
|
||||||
|
- sect233r1
|
||||||
|
- sect283k1
|
||||||
|
- sect283r1
|
||||||
|
- sect409k1
|
||||||
|
- sect409r1
|
||||||
|
- sect571k1
|
||||||
|
- sect571r1
|
||||||
|
passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the private key.
|
||||||
|
type: str
|
||||||
|
cipher:
|
||||||
|
description:
|
||||||
|
- The cipher to encrypt the private key. This is only used when O(passphrase) is provided.
|
||||||
|
- Must be V(auto).
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- Determines which crypto backend to use.
|
||||||
|
- The default choice is V(auto), which tries to use C(cryptography) if available.
|
||||||
|
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices: [auto, cryptography]
|
||||||
|
format:
|
||||||
|
description:
|
||||||
|
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format) is used for
|
||||||
|
all keys which support it. Please note that not every key can be exported in any format.
|
||||||
|
- The value V(auto) selects a format based on the key format. The value V(auto_ignore) does the same, but for existing
|
||||||
|
private key files, it will not force a regenerate when its format is not the automatically selected one for generation.
|
||||||
|
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default. To change this
|
||||||
|
behavior, use the O(format_mismatch) option.
|
||||||
|
type: str
|
||||||
|
default: auto_ignore
|
||||||
|
choices: [pkcs1, pkcs8, raw, auto, auto_ignore]
|
||||||
|
format_mismatch:
|
||||||
|
description:
|
||||||
|
- Determines behavior of the module if the format of a private key does not match the expected format, but all other
|
||||||
|
parameters are as expected.
|
||||||
|
- If set to V(regenerate) (default), generates a new private key.
|
||||||
|
- If set to V(convert), the key will be converted to the new format instead.
|
||||||
|
- Only supported by the C(cryptography) backend.
|
||||||
|
type: str
|
||||||
|
default: regenerate
|
||||||
|
choices: [regenerate, convert]
|
||||||
|
regenerate:
|
||||||
|
description:
|
||||||
|
- Allows to configure in which situations the module is allowed to regenerate private keys. The module will always generate
|
||||||
|
a new key if the destination file does not exist.
|
||||||
|
- By default, the key will be regenerated when it does not match the module's options, except when the key cannot be
|
||||||
|
read or the passphrase does not match. Please note that this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior
|
||||||
|
was as if V(full_idempotence) is specified.
|
||||||
|
- If set to V(never), the module will fail if the key cannot be read or the passphrase is not matching, and will never
|
||||||
|
regenerate an existing key.
|
||||||
|
- If set to V(fail), the module will fail if the key does not correspond to the module's options.
|
||||||
|
- If set to V(partial_idempotence), the key will be regenerated if it does not conform to the module's options. The
|
||||||
|
key is B(not) regenerated if it cannot be read (broken file), the key is protected by an unknown passphrase, or when
|
||||||
|
they key is not protected by a passphrase, but a passphrase is specified.
|
||||||
|
- If set to V(full_idempotence), the key will be regenerated if it does not conform to the module's options. This is
|
||||||
|
also the case if the key cannot be read (broken file), the key is protected by an unknown passphrase, or when they
|
||||||
|
key is not protected by a passphrase, but a passphrase is specified. Make sure you have a B(backup) when using this
|
||||||
|
option!
|
||||||
|
- If set to V(always), the module will always regenerate the key. This is equivalent to setting O(force) to V(true).
|
||||||
|
- Note that if O(format_mismatch) is set to V(convert) and everything matches except the format, the key will always
|
||||||
|
be converted, except if O(regenerate) is set to V(always).
|
||||||
|
type: str
|
||||||
|
choices:
|
||||||
|
- never
|
||||||
|
- fail
|
||||||
|
- partial_idempotence
|
||||||
|
- full_idempotence
|
||||||
|
- always
|
||||||
|
default: full_idempotence
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.x509_certificate
|
||||||
|
- module: community.crypto.x509_certificate_pipe
|
||||||
|
- module: community.crypto.openssl_csr
|
||||||
|
- module: community.crypto.openssl_csr_pipe
|
||||||
|
- module: community.crypto.openssl_dhparam
|
||||||
|
- module: community.crypto.openssl_pkcs12
|
||||||
|
- module: community.crypto.openssl_publickey
|
||||||
|
"""
|
||||||
52
plugins/doc_fragments/module_privatekey_convert.py
Normal file
52
plugins/doc_fragments/module_privatekey_convert.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
|
# Standard files documentation fragment
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
requirements:
|
||||||
|
- cryptography >= 1.2.3 (older versions might work as well)
|
||||||
|
attributes:
|
||||||
|
diff_mode:
|
||||||
|
support: none
|
||||||
|
idempotent:
|
||||||
|
support: full
|
||||||
|
options:
|
||||||
|
src_path:
|
||||||
|
description:
|
||||||
|
- Name of the file containing the OpenSSL private key to convert.
|
||||||
|
- Exactly one of O(src_path) or O(src_content) must be specified.
|
||||||
|
type: path
|
||||||
|
src_content:
|
||||||
|
description:
|
||||||
|
- The content of the file containing the OpenSSL private key to convert.
|
||||||
|
- Exactly one of O(src_path) or O(src_content) must be specified.
|
||||||
|
type: str
|
||||||
|
src_passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the private key to load.
|
||||||
|
type: str
|
||||||
|
dest_passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the private key to store.
|
||||||
|
type: str
|
||||||
|
format:
|
||||||
|
description:
|
||||||
|
- Determines which format the destination private key should be written in.
|
||||||
|
- Please note that not every key can be exported in any format, and that not every format supports encryption.
|
||||||
|
type: str
|
||||||
|
choices: [pkcs1, pkcs8, raw]
|
||||||
|
required: true
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.openssl_privatekey
|
||||||
|
- module: community.crypto.openssl_privatekey_pipe
|
||||||
|
- module: community.crypto.openssl_publickey
|
||||||
|
"""
|
||||||
32
plugins/doc_fragments/name_encoding.py
Normal file
32
plugins/doc_fragments/name_encoding.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
options:
|
||||||
|
name_encoding:
|
||||||
|
description:
|
||||||
|
- How to encode names (DNS names, URIs, email addresses) in return values.
|
||||||
|
- V(ignore) will use the encoding returned by the backend.
|
||||||
|
- V(idna) will convert all labels of domain names to IDNA encoding. IDNA2008 will be preferred, and IDNA2003 will be
|
||||||
|
used if IDNA2008 encoding fails.
|
||||||
|
- V(unicode) will convert all labels of domain names to Unicode. IDNA2008 will be preferred, and IDNA2003 will be used
|
||||||
|
if IDNA2008 decoding fails.
|
||||||
|
- B(Note) that V(idna) and V(unicode) require the L(idna Python library,https://pypi.org/project/idna/) to be installed.
|
||||||
|
type: str
|
||||||
|
default: ignore
|
||||||
|
choices:
|
||||||
|
- ignore
|
||||||
|
- idna
|
||||||
|
- unicode
|
||||||
|
requirements:
|
||||||
|
- If O(name_encoding) is set to another value than V(ignore), the L(idna Python library,https://pypi.org/project/idna/)
|
||||||
|
needs to be installed.
|
||||||
|
"""
|
||||||
68
plugins/filter/gpg_fingerprint.py
Normal file
68
plugins/filter/gpg_fingerprint.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: gpg_fingerprint
|
||||||
|
short_description: Retrieve a GPG fingerprint from a GPG public or private key
|
||||||
|
author: Felix Fontein (@felixfontein)
|
||||||
|
version_added: 2.15.0
|
||||||
|
description:
|
||||||
|
- Takes the content of a private or public GPG key as input and returns its fingerprint.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The content of a GPG public or private key.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
requirements:
|
||||||
|
- GnuPG (C(gpg) executable)
|
||||||
|
seealso:
|
||||||
|
- plugin: community.crypto.gpg_fingerprint
|
||||||
|
plugin_type: lookup
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show fingerprint of GPG public key
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ lookup('file', '/path/to/public_key.gpg') | community.crypto.gpg_fingerprint }}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- The fingerprint of the provided public or private GPG key.
|
||||||
|
type: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, get_fingerprint_from_bytes
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import PluginGPGRunner
|
||||||
|
|
||||||
|
|
||||||
|
def gpg_fingerprint(input):
|
||||||
|
if not isinstance(input, string_types):
|
||||||
|
raise AnsibleFilterError(
|
||||||
|
'The input for the community.crypto.gpg_fingerprint filter must be a string; got {type} instead'.format(type=type(input))
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
gpg = PluginGPGRunner()
|
||||||
|
return get_fingerprint_from_bytes(gpg, to_bytes(input))
|
||||||
|
except GPGError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'gpg_fingerprint': gpg_fingerprint,
|
||||||
|
}
|
||||||
316
plugins/filter/openssl_csr_info.py
Normal file
316
plugins/filter/openssl_csr_info.py
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: openssl_csr_info
|
||||||
|
short_description: Retrieve information from OpenSSL Certificate Signing Requests (CSR)
|
||||||
|
version_added: 2.10.0
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
description:
|
||||||
|
- Provided an OpenSSL Certificate Signing Requests (CSR), retrieve information.
|
||||||
|
- This is a filter version of the M(community.crypto.openssl_csr_info) module.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The content of the OpenSSL CSR.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.openssl_csr_info
|
||||||
|
- plugin: community.crypto.to_serial
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show the Subject Alt Names of the CSR
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
lookup('ansible.builtin.file', '/path/to/cert.csr')
|
||||||
|
| community.crypto.openssl_csr_info
|
||||||
|
).subject_alt_name | join(', ')
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- Information on the certificate.
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
signature_valid:
|
||||||
|
description:
|
||||||
|
- Whether the CSR's signature is valid.
|
||||||
|
- In case the check returns V(false), the module will fail.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
basic_constraints:
|
||||||
|
description: Entries in the C(basic_constraints) extension, or V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: ['CA:TRUE', 'pathlen:1']
|
||||||
|
basic_constraints_critical:
|
||||||
|
description: Whether the C(basic_constraints) extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
extended_key_usage:
|
||||||
|
description: Entries in the C(extended_key_usage) extension, or V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
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.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
critical:
|
||||||
|
description: Whether the extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
value:
|
||||||
|
description:
|
||||||
|
- The Base64 encoded value (in DER format) of the extension.
|
||||||
|
- B(Note) that depending on the C(cryptography) version used, it is not possible to extract the ASN.1 content
|
||||||
|
of the extension, but only to provide the re-encoded content of the extension in case it was parsed by C(cryptography).
|
||||||
|
This should usually result in exactly the same value, except if the original extension value was malformed.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: "MAMCAQU="
|
||||||
|
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 V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: [Key Agreement, Data Encipherment]
|
||||||
|
key_usage_critical:
|
||||||
|
description: Whether the C(key_usage) extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
subject_alt_name:
|
||||||
|
description:
|
||||||
|
- Entries in the C(subject_alt_name) extension, or V(none) if extension is not present.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
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
|
||||||
|
type: bool
|
||||||
|
ocsp_must_staple:
|
||||||
|
description: V(true) if the OCSP Must Staple extension is present, V(none) otherwise.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
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']
|
||||||
|
name_constraints_excluded:
|
||||||
|
description:
|
||||||
|
- List of excluded subtrees the CA cannot sign certificates for.
|
||||||
|
- Is V(none) if extension is not present.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: ['email:.com']
|
||||||
|
name_constraints_critical:
|
||||||
|
description:
|
||||||
|
- Whether the C(name_constraints) extension is critical.
|
||||||
|
- Is V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
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"}
|
||||||
|
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"]]
|
||||||
|
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 V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
|
||||||
|
- Will start with C(unknown) if the key type cannot be determined.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: RSA
|
||||||
|
public_key_data:
|
||||||
|
description:
|
||||||
|
- Public key data. Depends on the public key's type.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Bit size of modulus (RSA) or prime number (DSA).
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=RSA) or RV(_value.public_key_type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.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 RV(_value.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 RV(_value.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 RV(_value.public_key_type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When RV(_value.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 RV(_value.public_key_type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For RV(_value.public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For RV(_value.public_key_type=DSA), this is the publicly known group element whose discrete logarithm with respect
|
||||||
|
to C(g) is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=DSA) or RV(_value.public_key_type=ECC)
|
||||||
|
public_key_fingerprints:
|
||||||
|
description:
|
||||||
|
- Fingerprints of CSR's 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..."
|
||||||
|
subject_key_identifier:
|
||||||
|
description:
|
||||||
|
- The CSR's subject key identifier.
|
||||||
|
- The identifier is returned in hexadecimal, with V(:) used to separate bytes.
|
||||||
|
- Is V(none) if the C(SubjectKeyIdentifier) extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||||
|
authority_key_identifier:
|
||||||
|
description:
|
||||||
|
- The CSR's authority key identifier.
|
||||||
|
- The identifier is returned in hexadecimal, with V(:) used to separate bytes.
|
||||||
|
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||||
|
authority_cert_issuer:
|
||||||
|
description:
|
||||||
|
- The CSR's authority cert issuer as a list of general names.
|
||||||
|
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
|
||||||
|
authority_cert_serial_number:
|
||||||
|
description:
|
||||||
|
- The CSR's authority cert serial number.
|
||||||
|
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as C(11:22:33),
|
||||||
|
you need to convert it to that form with P(community.crypto.to_serial#filter).
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 12345
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
|
||||||
|
get_csr_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
|
||||||
|
|
||||||
|
|
||||||
|
def openssl_csr_info_filter(data, name_encoding='ignore'):
|
||||||
|
'''Extract information from X.509 PEM certificate.'''
|
||||||
|
if not isinstance(data, string_types):
|
||||||
|
raise AnsibleFilterError('The community.crypto.openssl_csr_info input must be a text type, not %s' % type(data))
|
||||||
|
if not isinstance(name_encoding, string_types):
|
||||||
|
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
|
||||||
|
name_encoding = to_native(name_encoding)
|
||||||
|
if name_encoding not in ('ignore', 'idna', 'unicode'):
|
||||||
|
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
|
||||||
|
|
||||||
|
module = FilterModuleMock({'name_encoding': name_encoding})
|
||||||
|
try:
|
||||||
|
return get_csr_info(module, 'cryptography', content=to_bytes(data), validate_signature=True)
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'openssl_csr_info': openssl_csr_info_filter,
|
||||||
|
}
|
||||||
193
plugins/filter/openssl_privatekey_info.py
Normal file
193
plugins/filter/openssl_privatekey_info.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: openssl_privatekey_info
|
||||||
|
short_description: Retrieve information from OpenSSL private keys
|
||||||
|
version_added: 2.10.0
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
description:
|
||||||
|
- Provided an OpenSSL private keys, retrieve information.
|
||||||
|
- This is a filter version of the M(community.crypto.openssl_privatekey_info) module.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The content of the OpenSSL private key.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
passphrase:
|
||||||
|
description:
|
||||||
|
- The passphrase for the private key.
|
||||||
|
type: str
|
||||||
|
return_private_key_data:
|
||||||
|
description:
|
||||||
|
- Whether to return private key data.
|
||||||
|
- Only set this to V(true) when you want private information about this key to be extracted.
|
||||||
|
- B(WARNING:) you have to make sure that private key data is not accidentally logged!
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.openssl_privatekey_info
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show the Subject Alt Names of the CSR
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
lookup('ansible.builtin.file', '/path/to/cert.csr')
|
||||||
|
| community.crypto.openssl_privatekey_info
|
||||||
|
).subject_alt_name | join(', ')
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- Information on the certificate.
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
public_key:
|
||||||
|
description: Private key's public key in PEM format.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||||
|
public_key_fingerprints:
|
||||||
|
description:
|
||||||
|
- Fingerprints of private key's 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 V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
|
||||||
|
- Will start with V(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 RV(_value.type=RSA) or RV(_value.type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=RSA)
|
||||||
|
p:
|
||||||
|
description:
|
||||||
|
- The C(p) value for DSA.
|
||||||
|
- This is the prime modulus upon which arithmetic takes place.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.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 RV(_value.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 RV(_value.type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When RV(_value.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 RV(_value.type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For RV(_value.type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For RV(_value.type=DSA), this is the publicly known group element whose discrete logarithm with respect to C(g)
|
||||||
|
is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=DSA) or RV(_value.type=ECC)
|
||||||
|
private_data:
|
||||||
|
description:
|
||||||
|
- Private key data. Depends on key type.
|
||||||
|
returned: success and when O(return_private_key_data) is set to V(true)
|
||||||
|
type: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
|
||||||
|
PrivateKeyParseError,
|
||||||
|
get_privatekey_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
|
||||||
|
|
||||||
|
|
||||||
|
def openssl_privatekey_info_filter(data, passphrase=None, return_private_key_data=False):
|
||||||
|
'''Extract information from X.509 PEM certificate.'''
|
||||||
|
if not isinstance(data, string_types):
|
||||||
|
raise AnsibleFilterError('The community.crypto.openssl_privatekey_info input must be a text type, not %s' % type(data))
|
||||||
|
if passphrase is not None and not isinstance(passphrase, string_types):
|
||||||
|
raise AnsibleFilterError('The passphrase option must be a text type, not %s' % type(passphrase))
|
||||||
|
if not isinstance(return_private_key_data, bool):
|
||||||
|
raise AnsibleFilterError('The return_private_key_data option must be a boolean, not %s' % type(return_private_key_data))
|
||||||
|
|
||||||
|
module = FilterModuleMock({})
|
||||||
|
try:
|
||||||
|
result = get_privatekey_info(module, 'cryptography', content=to_bytes(data), passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||||
|
result.pop('can_parse_key', None)
|
||||||
|
result.pop('key_is_consistent', None)
|
||||||
|
return result
|
||||||
|
except PrivateKeyParseError as exc:
|
||||||
|
raise AnsibleFilterError(exc.error_message)
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'openssl_privatekey_info': openssl_privatekey_info_filter,
|
||||||
|
}
|
||||||
163
plugins/filter/openssl_publickey_info.py
Normal file
163
plugins/filter/openssl_publickey_info.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: openssl_publickey_info
|
||||||
|
short_description: Retrieve information from OpenSSL public keys in PEM format
|
||||||
|
version_added: 2.10.0
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
description:
|
||||||
|
- Provided a public key in OpenSSL PEM format, retrieve information.
|
||||||
|
- This is a filter version of the M(community.crypto.openssl_publickey_info) module.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The content of the OpenSSL PEM public key.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.openssl_publickey_info
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show the type of a public key
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
lookup('ansible.builtin.file', '/path/to/public-key.pem')
|
||||||
|
| community.crypto.openssl_publickey_info
|
||||||
|
).type
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- Information on the public key.
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
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 V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
|
||||||
|
- Will start with V(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 RV(_value.type=RSA) or RV(_value.type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=RSA)
|
||||||
|
p:
|
||||||
|
description:
|
||||||
|
- The C(p) value for DSA.
|
||||||
|
- This is the prime modulus upon which arithmetic takes place.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.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 RV(_value.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 RV(_value.type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When RV(_value.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 RV(_value.type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For RV(_value.type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For RV(_value.type=DSA), this is the publicly known group element whose discrete logarithm with respect to C(g)
|
||||||
|
is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.type=DSA) or RV(_value.type=ECC)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||||
|
|
||||||
|
from ansible_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,
|
||||||
|
get_publickey_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
|
||||||
|
|
||||||
|
|
||||||
|
def openssl_publickey_info_filter(data):
|
||||||
|
'''Extract information from OpenSSL PEM public key.'''
|
||||||
|
if not isinstance(data, string_types):
|
||||||
|
raise AnsibleFilterError('The community.crypto.openssl_publickey_info input must be a text type, not %s' % type(data))
|
||||||
|
|
||||||
|
module = FilterModuleMock({})
|
||||||
|
try:
|
||||||
|
return get_publickey_info(module, 'cryptography', content=to_bytes(data))
|
||||||
|
except PublicKeyParseError as exc:
|
||||||
|
raise AnsibleFilterError(exc.error_message)
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'openssl_publickey_info': openssl_publickey_info_filter,
|
||||||
|
}
|
||||||
66
plugins/filter/parse_serial.py
Normal file
66
plugins/filter/parse_serial.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: parse_serial
|
||||||
|
short_description: Convert a serial number as a colon-separated list of hex numbers to an integer
|
||||||
|
author: Felix Fontein (@felixfontein)
|
||||||
|
version_added: 2.18.0
|
||||||
|
description:
|
||||||
|
- Parses a colon-separated list of hex numbers of the form C(00:11:22:33) and returns the corresponding integer.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- A serial number represented as a colon-separated list of hex numbers between 0 and 255.
|
||||||
|
- These numbers are interpreted as the byte presentation of an unsigned integer in network byte order. That is, C(01:00)
|
||||||
|
is interpreted as the integer 256.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
seealso:
|
||||||
|
- plugin: community.crypto.to_serial
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Parse serial number
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ '11:22:33' | community.crypto.parse_serial }}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- The serial number as an integer.
|
||||||
|
type: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.serial import parse_serial
|
||||||
|
|
||||||
|
|
||||||
|
def parse_serial_filter(input):
|
||||||
|
if not isinstance(input, string_types):
|
||||||
|
raise AnsibleFilterError(
|
||||||
|
'The input for the community.crypto.parse_serial filter must be a string; got {type} instead'.format(type=type(input))
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return parse_serial(to_native(input))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'parse_serial': parse_serial_filter,
|
||||||
|
}
|
||||||
64
plugins/filter/split_pem.py
Normal file
64
plugins/filter/split_pem.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: split_pem
|
||||||
|
short_description: Split PEM file contents into multiple objects
|
||||||
|
version_added: 2.10.0
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
description:
|
||||||
|
- Split PEM file contents into multiple PEM objects. Comments or invalid parts are ignored.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The PEM contents to split.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Print all CA certificates
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: '{{ item }}'
|
||||||
|
loop: >-
|
||||||
|
{{ lookup('ansible.builtin.file', '/path/to/ca-bundle.pem') | community.crypto.split_pem }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- A list of PEM file contents.
|
||||||
|
type: list
|
||||||
|
elements: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.module_utils.common.text.converters import to_text
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import split_pem_list
|
||||||
|
|
||||||
|
|
||||||
|
def split_pem_filter(data):
|
||||||
|
'''Split PEM file.'''
|
||||||
|
if not isinstance(data, string_types):
|
||||||
|
raise AnsibleFilterError('The community.crypto.split_pem input must be a text type, not %s' % type(data))
|
||||||
|
|
||||||
|
data = to_text(data)
|
||||||
|
return split_pem_list(data)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'split_pem': split_pem_filter,
|
||||||
|
}
|
||||||
68
plugins/filter/to_serial.py
Normal file
68
plugins/filter/to_serial.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: to_serial
|
||||||
|
short_description: Convert an integer to a colon-separated list of hex numbers
|
||||||
|
author: Felix Fontein (@felixfontein)
|
||||||
|
version_added: 2.18.0
|
||||||
|
description:
|
||||||
|
- Converts an integer to a colon-separated list of hex numbers of the form C(00:11:22:33).
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The non-negative integer to convert.
|
||||||
|
type: int
|
||||||
|
required: true
|
||||||
|
seealso:
|
||||||
|
- plugin: community.crypto.to_serial
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Convert integer to serial number
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ 1234567 | community.crypto.to_serial }}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- A colon-separated list of hexadecimal numbers.
|
||||||
|
- Letters are upper-case, and all numbers have exactly two digits.
|
||||||
|
- The string is never empty. The representation of C(0) is C("00").
|
||||||
|
type: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
from ansible.module_utils.six import integer_types
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.serial import to_serial
|
||||||
|
|
||||||
|
|
||||||
|
def to_serial_filter(input):
|
||||||
|
if not isinstance(input, integer_types):
|
||||||
|
raise AnsibleFilterError(
|
||||||
|
'The input for the community.crypto.to_serial filter must be an integer; got {type} instead'.format(type=type(input))
|
||||||
|
)
|
||||||
|
if input < 0:
|
||||||
|
raise AnsibleFilterError('The input for the community.crypto.to_serial filter must not be negative')
|
||||||
|
try:
|
||||||
|
return to_serial(input)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'to_serial': to_serial_filter,
|
||||||
|
}
|
||||||
350
plugins/filter/x509_certificate_info.py
Normal file
350
plugins/filter/x509_certificate_info.py
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: x509_certificate_info
|
||||||
|
short_description: Retrieve information from X.509 certificates in PEM format
|
||||||
|
version_added: 2.10.0
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
description:
|
||||||
|
- Provided a X.509 certificate in PEM format, retrieve information.
|
||||||
|
- This is a filter version of the M(community.crypto.x509_certificate_info) module.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The content of the X.509 certificate in PEM format.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.x509_certificate_info
|
||||||
|
- plugin: community.crypto.to_serial
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show the Subject Alt Names of the certificate
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
lookup('ansible.builtin.file', '/path/to/cert.pem')
|
||||||
|
| community.crypto.x509_certificate_info
|
||||||
|
).subject_alt_name | join(', ')
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- Information on the certificate.
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
expired:
|
||||||
|
description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
basic_constraints:
|
||||||
|
description: Entries in the C(basic_constraints) extension, or V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: ["CA:TRUE", "pathlen:1"]
|
||||||
|
basic_constraints_critical:
|
||||||
|
description: Whether the C(basic_constraints) extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
extended_key_usage:
|
||||||
|
description: Entries in the C(extended_key_usage) extension, or V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
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.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
critical:
|
||||||
|
description: Whether the extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
value:
|
||||||
|
description:
|
||||||
|
- The Base64 encoded value (in DER format) of the extension.
|
||||||
|
- B(Note) that depending on the C(cryptography) version used, it is not possible to extract the ASN.1 content
|
||||||
|
of the extension, but only to provide the re-encoded content of the extension in case it was parsed by C(cryptography).
|
||||||
|
This should usually result in exactly the same value, except if the original extension value was malformed.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: "MAMCAQU="
|
||||||
|
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 V(none) if extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: [Key Agreement, Data Encipherment]
|
||||||
|
key_usage_critical:
|
||||||
|
description: Whether the C(key_usage) extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
subject_alt_name:
|
||||||
|
description:
|
||||||
|
- Entries in the C(subject_alt_name) extension, or V(none) if extension is not present.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
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
|
||||||
|
type: bool
|
||||||
|
ocsp_must_staple:
|
||||||
|
description: V(true) if the OCSP Must Staple extension is present, V(none) otherwise.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
ocsp_must_staple_critical:
|
||||||
|
description: Whether the C(ocsp_must_staple) extension is critical.
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
issuer:
|
||||||
|
description:
|
||||||
|
- The certificate's issuer.
|
||||||
|
- Note that for repeated values, only the last one will be returned.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
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"]]
|
||||||
|
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"}
|
||||||
|
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"]]
|
||||||
|
not_after:
|
||||||
|
description: C(notAfter) date as ASN.1 TIME.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '20190413202428Z'
|
||||||
|
not_before:
|
||||||
|
description: C(notBefore) date as ASN.1 TIME.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '20190331202428Z'
|
||||||
|
public_key:
|
||||||
|
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 V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
|
||||||
|
- Will start with V(unknown) if the key type cannot be determined.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: RSA
|
||||||
|
public_key_data:
|
||||||
|
description:
|
||||||
|
- Public key data. Depends on the public key's type.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Bit size of modulus (RSA) or prime number (DSA).
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=RSA) or RV(_value.public_key_type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.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 RV(_value.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 RV(_value.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 RV(_value.public_key_type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When RV(_value.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 RV(_value.public_key_type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For RV(_value.public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For RV(_value.public_key_type=DSA), this is the publicly known group element whose discrete logarithm with respect
|
||||||
|
to C(g) is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When RV(_value.public_key_type=DSA) or RV(_value.public_key_type=ECC)
|
||||||
|
public_key_fingerprints:
|
||||||
|
description:
|
||||||
|
- Fingerprints of certificate's 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..."
|
||||||
|
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..."
|
||||||
|
signature_algorithm:
|
||||||
|
description: The signature algorithm used to sign the certificate.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: sha256WithRSAEncryption
|
||||||
|
serial_number:
|
||||||
|
description:
|
||||||
|
- The certificate's serial number.
|
||||||
|
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as C(11:22:33),
|
||||||
|
you need to convert it to that form with P(community.crypto.to_serial#filter).
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 1234
|
||||||
|
version:
|
||||||
|
description: The certificate version.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 3
|
||||||
|
subject_key_identifier:
|
||||||
|
description:
|
||||||
|
- The certificate's subject key identifier.
|
||||||
|
- The identifier is returned in hexadecimal, with V(:) used to separate bytes.
|
||||||
|
- Is V(none) if the C(SubjectKeyIdentifier) extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||||
|
authority_key_identifier:
|
||||||
|
description:
|
||||||
|
- The certificate's authority key identifier.
|
||||||
|
- The identifier is returned in hexadecimal, with V(:) used to separate bytes.
|
||||||
|
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||||
|
authority_cert_issuer:
|
||||||
|
description:
|
||||||
|
- The certificate's authority cert issuer as a list of general names.
|
||||||
|
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
|
||||||
|
authority_cert_serial_number:
|
||||||
|
description:
|
||||||
|
- The certificate's authority cert serial number.
|
||||||
|
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as C(11:22:33),
|
||||||
|
you need to convert it to that form with P(community.crypto.to_serial#filter).
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 12345
|
||||||
|
ocsp_uri:
|
||||||
|
description: The OCSP responder URI, if included in the certificate. Will be V(none) if no OCSP responder URI is included.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
issuer_uri:
|
||||||
|
description: The Issuer URI, if included in the certificate. Will be V(none) if no issuer URI is included.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
|
||||||
|
get_certificate_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
|
||||||
|
|
||||||
|
|
||||||
|
def x509_certificate_info_filter(data, name_encoding='ignore'):
|
||||||
|
'''Extract information from X.509 PEM certificate.'''
|
||||||
|
if not isinstance(data, string_types):
|
||||||
|
raise AnsibleFilterError('The community.crypto.x509_certificate_info input must be a text type, not %s' % type(data))
|
||||||
|
if not isinstance(name_encoding, string_types):
|
||||||
|
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
|
||||||
|
name_encoding = to_native(name_encoding)
|
||||||
|
if name_encoding not in ('ignore', 'idna', 'unicode'):
|
||||||
|
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
|
||||||
|
|
||||||
|
module = FilterModuleMock({'name_encoding': name_encoding})
|
||||||
|
try:
|
||||||
|
return get_certificate_info(module, 'cryptography', content=to_bytes(data))
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'x509_certificate_info': x509_certificate_info_filter,
|
||||||
|
}
|
||||||
211
plugins/filter/x509_crl_info.py
Normal file
211
plugins/filter/x509_crl_info.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: x509_crl_info
|
||||||
|
short_description: Retrieve information from X.509 CRLs in PEM format
|
||||||
|
version_added: 2.10.0
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
description:
|
||||||
|
- Provided a X.509 crl in PEM format, retrieve information.
|
||||||
|
- This is a filter version of the M(community.crypto.x509_crl_info) module.
|
||||||
|
options:
|
||||||
|
_input:
|
||||||
|
description:
|
||||||
|
- The content of the X.509 CRL in PEM format.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
list_revoked_certificates:
|
||||||
|
description:
|
||||||
|
- If set to V(false), the list of revoked certificates is not included in the result.
|
||||||
|
- This is useful when retrieving information on large CRL files. Enumerating all revoked 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
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.x509_crl_info
|
||||||
|
- plugin: community.crypto.to_serial
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show the Organization Name of the CRL's subject
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
lookup('ansible.builtin.file', '/path/to/cert.pem')
|
||||||
|
| community.crypto.x509_crl_info
|
||||||
|
).issuer.organizationName
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- Information on the CRL.
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
format:
|
||||||
|
description:
|
||||||
|
- Whether the CRL is in PEM format (V(pem)) or in DER format (V(der)).
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: pem
|
||||||
|
choices:
|
||||||
|
- pem
|
||||||
|
- der
|
||||||
|
issuer:
|
||||||
|
description:
|
||||||
|
- The CRL's issuer.
|
||||||
|
- Note that for repeated values, only the last one will be returned.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
|
||||||
|
issuer_ordered:
|
||||||
|
description: The CRL's issuer as an ordered list of tuples.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
elements: list
|
||||||
|
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
|
||||||
|
last_update:
|
||||||
|
description: The point in time from which this CRL can be trusted as ASN.1 TIME.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '20190413202428Z'
|
||||||
|
next_update:
|
||||||
|
description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: '20190413202428Z'
|
||||||
|
digest:
|
||||||
|
description: The signature algorithm used to sign the CRL.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: sha256WithRSAEncryption
|
||||||
|
revoked_certificates:
|
||||||
|
description: List of certificates to be revoked.
|
||||||
|
returned: success if O(list_revoked_certificates=true)
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
contains:
|
||||||
|
serial_number:
|
||||||
|
description:
|
||||||
|
- Serial number of the certificate.
|
||||||
|
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as
|
||||||
|
C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
|
||||||
|
type: int
|
||||||
|
sample: 1234
|
||||||
|
revocation_date:
|
||||||
|
description: The point in time the certificate was revoked as ASN.1 TIME.
|
||||||
|
type: str
|
||||||
|
sample: '20190413202428Z'
|
||||||
|
issuer:
|
||||||
|
description:
|
||||||
|
- The certificate's issuer.
|
||||||
|
- See O(name_encoding) for how IDNs are handled.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: ["DNS:ca.example.org"]
|
||||||
|
issuer_critical:
|
||||||
|
description: Whether the certificate issuer extension is critical.
|
||||||
|
type: bool
|
||||||
|
sample: false
|
||||||
|
reason:
|
||||||
|
description:
|
||||||
|
- The value for the revocation reason extension.
|
||||||
|
type: str
|
||||||
|
sample: key_compromise
|
||||||
|
choices:
|
||||||
|
- unspecified
|
||||||
|
- key_compromise
|
||||||
|
- ca_compromise
|
||||||
|
- affiliation_changed
|
||||||
|
- superseded
|
||||||
|
- cessation_of_operation
|
||||||
|
- certificate_hold
|
||||||
|
- privilege_withdrawn
|
||||||
|
- aa_compromise
|
||||||
|
- remove_from_crl
|
||||||
|
reason_critical:
|
||||||
|
description: Whether the revocation reason extension is critical.
|
||||||
|
type: bool
|
||||||
|
sample: false
|
||||||
|
invalidity_date:
|
||||||
|
description: |-
|
||||||
|
The point in time it was known/suspected that the private key was compromised
|
||||||
|
or that the certificate otherwise became invalid as ASN.1 TIME.
|
||||||
|
type: str
|
||||||
|
sample: '20190413202428Z'
|
||||||
|
invalidity_date_critical:
|
||||||
|
description: Whether the invalidity date extension is critical.
|
||||||
|
type: bool
|
||||||
|
sample: false
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from ansible.module_utils.six import string_types
|
||||||
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||||
|
identify_pem_format,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
|
||||||
|
get_crl_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
|
||||||
|
|
||||||
|
|
||||||
|
def x509_crl_info_filter(data, name_encoding='ignore', list_revoked_certificates=True):
|
||||||
|
'''Extract information from X.509 PEM certificate.'''
|
||||||
|
if not isinstance(data, string_types):
|
||||||
|
raise AnsibleFilterError('The community.crypto.x509_crl_info input must be a text type, not %s' % type(data))
|
||||||
|
if not isinstance(name_encoding, string_types):
|
||||||
|
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
|
||||||
|
if not isinstance(list_revoked_certificates, bool):
|
||||||
|
raise AnsibleFilterError('The list_revoked_certificates option must be a boolean, not %s' % type(list_revoked_certificates))
|
||||||
|
name_encoding = to_native(name_encoding)
|
||||||
|
if name_encoding not in ('ignore', 'idna', 'unicode'):
|
||||||
|
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
|
||||||
|
|
||||||
|
data = to_bytes(data)
|
||||||
|
if not identify_pem_format(data):
|
||||||
|
try:
|
||||||
|
data = base64.b64decode(to_native(data))
|
||||||
|
except (binascii.Error, TypeError, ValueError, UnicodeEncodeError) as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
module = FilterModuleMock({'name_encoding': name_encoding})
|
||||||
|
try:
|
||||||
|
return get_crl_info(module, content=data, list_revoked_certificates=list_revoked_certificates)
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise AnsibleFilterError(to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Ansible jinja2 filters'''
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'x509_crl_info': x509_crl_info_filter,
|
||||||
|
}
|
||||||
64
plugins/lookup/gpg_fingerprint.py
Normal file
64
plugins/lookup/gpg_fingerprint.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
name: gpg_fingerprint
|
||||||
|
short_description: Retrieve a GPG fingerprint from a GPG public or private key file
|
||||||
|
author: Felix Fontein (@felixfontein)
|
||||||
|
version_added: 2.15.0
|
||||||
|
description:
|
||||||
|
- Takes a list of filenames pointing to GPG public or private key files. Returns the fingerprints for each of these keys.
|
||||||
|
options:
|
||||||
|
_terms:
|
||||||
|
description:
|
||||||
|
- A path to a GPG public or private key.
|
||||||
|
type: list
|
||||||
|
elements: path
|
||||||
|
required: true
|
||||||
|
requirements:
|
||||||
|
- GnuPG (C(gpg) executable)
|
||||||
|
seealso:
|
||||||
|
- plugin: community.crypto.gpg_fingerprint
|
||||||
|
plugin_type: filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Show fingerprint of GPG public key
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ lookup('community.crypto.gpg_fingerprint', '/path/to/public_key.gpg') }}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
_value:
|
||||||
|
description:
|
||||||
|
- The fingerprints of the provided public or private GPG keys.
|
||||||
|
- The list has one entry for every path provided.
|
||||||
|
type: list
|
||||||
|
elements: string
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.plugins.lookup import LookupBase
|
||||||
|
from ansible.errors import AnsibleLookupError
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, get_fingerprint_from_file
|
||||||
|
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import PluginGPGRunner
|
||||||
|
|
||||||
|
|
||||||
|
class LookupModule(LookupBase):
|
||||||
|
def run(self, terms, variables=None, **kwargs):
|
||||||
|
self.set_options(direct=kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
gpg = PluginGPGRunner(cwd=self._loader.get_basedir())
|
||||||
|
result = []
|
||||||
|
for path in terms:
|
||||||
|
result.append(get_fingerprint_from_file(gpg, path))
|
||||||
|
return result
|
||||||
|
except GPGError as exc:
|
||||||
|
raise AnsibleLookupError(to_native(exc))
|
||||||
345
plugins/module_utils/_version.py
Normal file
345
plugins/module_utils/_version.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# Vendored copy of distutils/version.py from CPython 3.9.5
|
||||||
|
#
|
||||||
|
# Implements multiple version numbering conventions for the
|
||||||
|
# Python Module Distribution Utilities.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2001-2022 Python Software Foundation. All rights reserved.
|
||||||
|
# PSF License (see LICENSES/PSF-2.0.txt or https://opensource.org/licenses/Python-2.0)
|
||||||
|
# SPDX-License-Identifier: PSF-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
266
plugins/module_utils/acme/account.py
Normal file
266
plugins/module_utils/acme/account.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.common._collections_compat import Mapping
|
||||||
|
|
||||||
|
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 registration calls without contacts, even
|
||||||
|
# if onlyReturnExisting is set to true.
|
||||||
|
created, data = self._new_reg(contact=contact, allow_creation=False)
|
||||||
|
if data:
|
||||||
|
# An account already exists! Return data
|
||||||
|
return created, data
|
||||||
|
# An account does not yet exist. Try to create one next.
|
||||||
|
|
||||||
|
new_reg = {
|
||||||
|
'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 not isinstance(result, Mapping):
|
||||||
|
raise ACMEProtocolException(
|
||||||
|
self.client.module, msg='Invalid account creation reply from ACME server', info=info, content=result)
|
||||||
|
|
||||||
|
if info['status'] in ([200, 201] if self.client.version == 1 else [201]):
|
||||||
|
# Account did not exist
|
||||||
|
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'] in (400, 404) and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation:
|
||||||
|
# Account does not exist (and we did not try to create it)
|
||||||
|
# (According to RFC 8555, Section 7.3.1, the HTTP status code MUST be 400.
|
||||||
|
# Unfortunately Digicert does not care and sends 404 instead.)
|
||||||
|
return False, None
|
||||||
|
elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''):
|
||||||
|
# Account has been deactivated; currently works for Pebble; has not been
|
||||||
|
# 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 not isinstance(result, Mapping):
|
||||||
|
raise ACMEProtocolException(
|
||||||
|
self.client.module, msg='Invalid account data retrieved from ACME server', info=info, content=result)
|
||||||
|
if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
|
||||||
|
# Returned when account is deactivated
|
||||||
|
return None
|
||||||
|
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, info = self.client.send_signed_request(self.client.account_uri, update_request)
|
||||||
|
if not isinstance(account_data, Mapping):
|
||||||
|
raise ACMEProtocolException(
|
||||||
|
self.client.module, msg='Invalid account updating reply from ACME server', info=info, content=account_data)
|
||||||
|
|
||||||
|
return True, account_data
|
||||||
539
plugins/module_utils/acme/acme.py
Normal file
539
plugins/module_utils/acme/acme.py
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import locale
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
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.argspec import ArgumentSpec
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||||
|
OpenSSLCLIBackend,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
|
||||||
|
CryptographyBackend,
|
||||||
|
CRYPTOGRAPHY_ERROR,
|
||||||
|
CRYPTOGRAPHY_MINIMAL_VERSION,
|
||||||
|
CRYPTOGRAPHY_VERSION,
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||||
|
ACMEProtocolException,
|
||||||
|
NetworkException,
|
||||||
|
ModuleFailException,
|
||||||
|
KeyParsingError,
|
||||||
|
format_http_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||||
|
compute_cert_id,
|
||||||
|
nopad_b64,
|
||||||
|
parse_retry_after,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ipaddress # noqa: F401, pylint: disable=unused-import
|
||||||
|
except ImportError:
|
||||||
|
HAS_IPADDRESS = False
|
||||||
|
IPADDRESS_IMPORT_ERROR = traceback.format_exc()
|
||||||
|
else:
|
||||||
|
HAS_IPADDRESS = True
|
||||||
|
IPADDRESS_IMPORT_ERROR = None
|
||||||
|
|
||||||
|
|
||||||
|
# -1 usually means connection problems
|
||||||
|
RETRY_STATUS_CODES = (-1, 408, 429, 503)
|
||||||
|
|
||||||
|
RETRY_COUNT = 10
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_retry(module, response, info, retry_count):
|
||||||
|
if info['status'] not in RETRY_STATUS_CODES:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if retry_count >= RETRY_COUNT:
|
||||||
|
raise ACMEProtocolException(
|
||||||
|
module, msg='Giving up after {retry} retries'.format(retry=RETRY_COUNT), info=info, response=response)
|
||||||
|
|
||||||
|
# 429 and 503 should have a Retry-After header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
|
||||||
|
try:
|
||||||
|
retry_after = min(max(1, int(info.get('retry-after'))), 60)
|
||||||
|
except (TypeError, ValueError) as dummy:
|
||||||
|
retry_after = 10
|
||||||
|
module.log('Retrieved a %s HTTP status on %s, retrying in %s seconds' % (format_http_status(info['status']), info['url'], retry_after))
|
||||||
|
|
||||||
|
time.sleep(retry_after)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.request_timeout = module.params['request_timeout']
|
||||||
|
|
||||||
|
# Check whether self.version matches what we expect
|
||||||
|
if self.version == 1:
|
||||||
|
for key in ('new-reg', 'new-authz', 'new-cert'):
|
||||||
|
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 __contains__(self, key):
|
||||||
|
return key in self.directory
|
||||||
|
|
||||||
|
def get(self, key, default_value=None):
|
||||||
|
return self.directory.get(key, default_value)
|
||||||
|
|
||||||
|
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
|
||||||
|
retry_count = 0
|
||||||
|
while True:
|
||||||
|
response, info = fetch_url(self.module, url, method='HEAD', timeout=self.request_timeout)
|
||||||
|
if _decode_retry(self.module, response, info, retry_count):
|
||||||
|
retry_count += 1
|
||||||
|
continue
|
||||||
|
if info['status'] not in (200, 204):
|
||||||
|
raise NetworkException("Failed to get replay-nonce, got status {0}".format(format_http_status(info['status'])))
|
||||||
|
if 'replay-nonce' in info:
|
||||||
|
return info['replay-nonce']
|
||||||
|
self.module.log(
|
||||||
|
'HEAD to {0} did return status {1}, but no replay-nonce header!'.format(url, format_http_status(info['status'])))
|
||||||
|
if retry_count >= 5:
|
||||||
|
raise ACMEProtocolException(
|
||||||
|
self.module, msg='Was not able to obtain nonce, giving up after 5 retries', info=info, response=response)
|
||||||
|
retry_count += 1
|
||||||
|
|
||||||
|
def has_renewal_info_endpoint(self):
|
||||||
|
return 'renewalInfo' in self.directory
|
||||||
|
|
||||||
|
|
||||||
|
class ACMEClient(object):
|
||||||
|
'''
|
||||||
|
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.get('account_key_src')
|
||||||
|
self.account_key_content = module.params.get('account_key_content')
|
||||||
|
self.account_key_passphrase = module.params.get('account_key_passphrase')
|
||||||
|
|
||||||
|
# Grab account URI from module parameters.
|
||||||
|
# Make sure empty string is treated as None.
|
||||||
|
self.account_uri = module.params.get('account_uri') or None
|
||||||
|
|
||||||
|
self.request_timeout = module.params['request_timeout']
|
||||||
|
|
||||||
|
self.account_key_data = None
|
||||||
|
self.account_jwk = None
|
||||||
|
self.account_jws_header = None
|
||||||
|
if self.account_key_file is not None or self.account_key_content is not None:
|
||||||
|
try:
|
||||||
|
self.account_key_data = self.parse_key(
|
||||||
|
key_file=self.account_key_file,
|
||||||
|
key_content=self.account_key_content,
|
||||||
|
passphrase=self.account_key_passphrase)
|
||||||
|
except KeyParsingError as e:
|
||||||
|
raise ModuleFailException("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', timeout=self.request_timeout)
|
||||||
|
if _decode_retry(self.module, resp, info, failed_tries):
|
||||||
|
failed_tries += 1
|
||||||
|
continue
|
||||||
|
_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
|
||||||
|
retry_count = 0
|
||||||
|
while True:
|
||||||
|
resp, info = fetch_url(self.module, uri, method='GET', headers=headers, timeout=self.request_timeout)
|
||||||
|
if not _decode_retry(self.module, resp, info, retry_count):
|
||||||
|
break
|
||||||
|
retry_count += 1
|
||||||
|
|
||||||
|
_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_renewal_info(
|
||||||
|
self,
|
||||||
|
cert_id=None,
|
||||||
|
cert_info=None,
|
||||||
|
cert_filename=None,
|
||||||
|
cert_content=None,
|
||||||
|
include_retry_after=False,
|
||||||
|
retry_after_relative_with_timezone=True,
|
||||||
|
):
|
||||||
|
if not self.directory.has_renewal_info_endpoint():
|
||||||
|
raise ModuleFailException('The ACME endpoint does not support ACME Renewal Information retrieval')
|
||||||
|
|
||||||
|
if cert_id is None:
|
||||||
|
cert_id = compute_cert_id(self.backend, cert_info=cert_info, cert_filename=cert_filename, cert_content=cert_content)
|
||||||
|
url = '{base}/{cert_id}'.format(base=self.directory.directory['renewalInfo'].rstrip('/'), cert_id=cert_id)
|
||||||
|
|
||||||
|
data, info = self.get_request(url, parse_json_result=True, fail_on_error=True, get_only=True)
|
||||||
|
|
||||||
|
# Include Retry-After header if asked for
|
||||||
|
if include_retry_after and 'retry-after' in info:
|
||||||
|
try:
|
||||||
|
data['retryAfter'] = parse_retry_after(
|
||||||
|
info['retry-after'],
|
||||||
|
relative_with_timezone=retry_after_relative_with_timezone,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_argspec():
|
||||||
|
'''
|
||||||
|
Provides default argument spec for the options documented in the acme doc fragment.
|
||||||
|
|
||||||
|
DEPRECATED: will be removed in community.crypto 3.0.0
|
||||||
|
'''
|
||||||
|
return dict(
|
||||||
|
acme_directory=dict(type='str', required=True),
|
||||||
|
acme_version=dict(type='int', required=True, choices=[1, 2]),
|
||||||
|
validate_certs=dict(type='bool', default=True),
|
||||||
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
|
||||||
|
request_timeout=dict(type='int', default=10),
|
||||||
|
account_key_src=dict(type='path', aliases=['account_key']),
|
||||||
|
account_key_content=dict(type='str', no_log=True),
|
||||||
|
account_key_passphrase=dict(type='str', no_log=True),
|
||||||
|
account_uri=dict(type='str'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_argspec(
|
||||||
|
with_account=True,
|
||||||
|
require_account_key=True,
|
||||||
|
with_certificate=False,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Provides default argument spec for the options documented in the acme doc fragment.
|
||||||
|
'''
|
||||||
|
result = ArgumentSpec(
|
||||||
|
argument_spec=dict(
|
||||||
|
acme_directory=dict(type='str', required=True),
|
||||||
|
acme_version=dict(type='int', required=True, choices=[1, 2]),
|
||||||
|
validate_certs=dict(type='bool', default=True),
|
||||||
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
|
||||||
|
request_timeout=dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if with_account:
|
||||||
|
result.update_argspec(
|
||||||
|
account_key_src=dict(type='path', aliases=['account_key']),
|
||||||
|
account_key_content=dict(type='str', no_log=True),
|
||||||
|
account_key_passphrase=dict(type='str', no_log=True),
|
||||||
|
account_uri=dict(type='str'),
|
||||||
|
)
|
||||||
|
if require_account_key:
|
||||||
|
result.update(required_one_of=[['account_key_src', 'account_key_content']])
|
||||||
|
result.update(mutually_exclusive=[['account_key_src', 'account_key_content']])
|
||||||
|
if with_certificate:
|
||||||
|
result.update_argspec(
|
||||||
|
csr=dict(type='path'),
|
||||||
|
csr_content=dict(type='str'),
|
||||||
|
)
|
||||||
|
result.update(
|
||||||
|
required_one_of=[['csr', 'csr_content']],
|
||||||
|
mutually_exclusive=[['csr', 'csr_content']],
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def create_backend(module, needs_acme_v2):
|
||||||
|
if not HAS_IPADDRESS:
|
||||||
|
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR)
|
||||||
|
|
||||||
|
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 CRYPTOGRAPHY_ERROR is not None:
|
||||||
|
# Either we could not import cryptography at all, or there was an unexpected error
|
||||||
|
if CRYPTOGRAPHY_VERSION is None:
|
||||||
|
msg = missing_required_lib('cryptography')
|
||||||
|
else:
|
||||||
|
msg = 'Unexpected error while preparing cryptography: {0}'.format(CRYPTOGRAPHY_ERROR.splitlines()[-1])
|
||||||
|
module.fail_json(msg=msg, exception=CRYPTOGRAPHY_ERROR)
|
||||||
|
if not HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
|
# We succeeded importing cryptography, but its version is too old.
|
||||||
|
module.fail_json(
|
||||||
|
msg='Found cryptography, but only version {0}. {1}'.format(
|
||||||
|
CRYPTOGRAPHY_VERSION,
|
||||||
|
missing_required_lib('cryptography >= {0}'.format(CRYPTOGRAPHY_MINIMAL_VERSION))))
|
||||||
|
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 needs_acme_v2 and module.params['acme_version'] < 2:
|
||||||
|
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
|
||||||
|
|
||||||
|
if module.params['acme_version'] == 1:
|
||||||
|
module.deprecate("The value 1 for 'acme_version' is deprecated. Please switch to ACME v2",
|
||||||
|
version='3.0.0', collection_name='community.crypto')
|
||||||
|
|
||||||
|
# AnsibleModule() changes the locale, so change it back to C because we rely
|
||||||
|
# on datetime.datetime.strptime() when parsing certificate dates.
|
||||||
|
locale.setlocale(locale.LC_ALL, 'C')
|
||||||
|
|
||||||
|
return module_backend
|
||||||
434
plugins/module_utils/acme/backend_cryptography.py
Normal file
434
plugins/module_utils/acme/backend_cryptography.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
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 (
|
||||||
|
CertificateInformation,
|
||||||
|
CryptoBackend,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
|
||||||
|
ChainMatcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||||
|
BackendException,
|
||||||
|
KeyParsingError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
|
||||||
|
convert_int_to_bytes,
|
||||||
|
convert_int_to_hex,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
cryptography_name_to_oid,
|
||||||
|
cryptography_serial_number_of_cert,
|
||||||
|
get_not_valid_after,
|
||||||
|
get_not_valid_before,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||||
|
extract_first_pem,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
parse_name_field,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import (
|
||||||
|
add_or_remove_timezone,
|
||||||
|
)
|
||||||
|
|
||||||
|
CRYPTOGRAPHY_MINIMAL_VERSION = '1.5'
|
||||||
|
|
||||||
|
CRYPTOGRAPHY_ERROR = None
|
||||||
|
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
|
||||||
|
except ImportError as dummy:
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY = False
|
||||||
|
CRYPTOGRAPHY_VERSION = None
|
||||||
|
CRYPTOGRAPHY_ERROR = traceback.format_exc()
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_VERSION = cryptography.__version__
|
||||||
|
HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion(CRYPTOGRAPHY_MINIMAL_VERSION))
|
||||||
|
try:
|
||||||
|
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||||
|
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||||
|
except Exception as dummy:
|
||||||
|
CRYPTOGRAPHY_ERROR = traceback.format_exc()
|
||||||
|
|
||||||
|
|
||||||
|
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, 'subject')
|
||||||
|
]
|
||||||
|
if criterium.issuer:
|
||||||
|
self.issuer = [
|
||||||
|
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer, 'issuer')
|
||||||
|
]
|
||||||
|
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
|
||||||
|
criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module)
|
||||||
|
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, with_timezone=CRYPTOGRAPHY_TIMEZONE)
|
||||||
|
|
||||||
|
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 is not 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(pk.e)),
|
||||||
|
"n": nopad_b64(convert_int_to_bytes(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(pk.x, count=num_bytes)),
|
||||||
|
"y": nopad_b64(convert_int_to_bytes(pk.y, count=num_bytes)),
|
||||||
|
},
|
||||||
|
'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 = convert_int_to_hex(r, 2 * key_data['point_size'])
|
||||||
|
ss = convert_int_to_hex(s, 2 * key_data['point_size'])
|
||||||
|
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"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_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
|
||||||
|
'''
|
||||||
|
Return a list of requested identifiers (CN and SANs) for the CSR.
|
||||||
|
Each identifier is a pair (type, identifier), where type is either
|
||||||
|
'dns' or 'ip'.
|
||||||
|
|
||||||
|
The list is deduplicated, and if a CNAME is present, it will be returned
|
||||||
|
as the first element in the result.
|
||||||
|
'''
|
||||||
|
if csr_content is None:
|
||||||
|
csr_content = read_file(csr_filename)
|
||||||
|
else:
|
||||||
|
csr_content = to_bytes(csr_content)
|
||||||
|
csr = cryptography.x509.load_pem_x509_csr(csr_content, _cryptography_backend)
|
||||||
|
|
||||||
|
identifiers = set()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def add_identifier(identifier):
|
||||||
|
if identifier in identifiers:
|
||||||
|
return
|
||||||
|
identifiers.add(identifier)
|
||||||
|
result.append(identifier)
|
||||||
|
|
||||||
|
for sub in csr.subject:
|
||||||
|
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
|
||||||
|
add_identifier(('dns', sub.value))
|
||||||
|
for extension in csr.extensions:
|
||||||
|
if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
|
||||||
|
for name in extension.value:
|
||||||
|
if isinstance(name, cryptography.x509.DNSName):
|
||||||
|
add_identifier(('dns', name.value))
|
||||||
|
elif isinstance(name, cryptography.x509.IPAddress):
|
||||||
|
add_identifier(('ip', name.value.compressed))
|
||||||
|
else:
|
||||||
|
raise BackendException('Found unsupported SAN identifier {0}'.format(name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
|
||||||
|
'''
|
||||||
|
Return a set of requested identifiers (CN and SANs) for the CSR.
|
||||||
|
Each identifier is a pair (type, identifier), where type is either
|
||||||
|
'dns' or 'ip'.
|
||||||
|
'''
|
||||||
|
return set(self.get_ordered_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
|
||||||
|
|
||||||
|
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
|
||||||
|
'''
|
||||||
|
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 = self.get_now()
|
||||||
|
else:
|
||||||
|
now = add_or_remove_timezone(now, with_timezone=CRYPTOGRAPHY_TIMEZONE)
|
||||||
|
return (get_not_valid_after(cert) - now).days
|
||||||
|
|
||||||
|
def create_chain_matcher(self, criterium):
|
||||||
|
'''
|
||||||
|
Given a Criterium object, creates a ChainMatcher object.
|
||||||
|
'''
|
||||||
|
return CryptographyChainMatcher(criterium, self.module)
|
||||||
|
|
||||||
|
def get_cert_information(self, cert_filename=None, cert_content=None):
|
||||||
|
'''
|
||||||
|
Return some information on a X.509 certificate as a CertificateInformation object.
|
||||||
|
'''
|
||||||
|
if cert_filename is not None:
|
||||||
|
cert_content = read_file(cert_filename)
|
||||||
|
else:
|
||||||
|
cert_content = to_bytes(cert_content)
|
||||||
|
|
||||||
|
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
|
||||||
|
cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend)
|
||||||
|
except Exception as e:
|
||||||
|
if cert_filename is None:
|
||||||
|
raise BackendException('Cannot parse certificate: {0}'.format(e))
|
||||||
|
raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e))
|
||||||
|
|
||||||
|
ski = None
|
||||||
|
try:
|
||||||
|
ext = cert.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier)
|
||||||
|
ski = ext.value.digest
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
aki = None
|
||||||
|
try:
|
||||||
|
ext = cert.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier)
|
||||||
|
aki = ext.value.key_identifier
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return CertificateInformation(
|
||||||
|
not_valid_after=get_not_valid_after(cert),
|
||||||
|
not_valid_before=get_not_valid_before(cert),
|
||||||
|
serial_number=cryptography_serial_number_of_cert(cert),
|
||||||
|
subject_key_identifier=ski,
|
||||||
|
authority_key_identifier=aki,
|
||||||
|
)
|
||||||
410
plugins/module_utils/acme/backend_openssl_cli.py
Normal file
410
plugins/module_utils/acme/backend_openssl_cli.py
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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 (
|
||||||
|
CertificateInformation,
|
||||||
|
CryptoBackend,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||||
|
BackendException,
|
||||||
|
KeyParsingError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import convert_bytes_to_int
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import ensure_utc_timezone
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ipaddress
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_date(out_text, name, cert_filename_suffix=""):
|
||||||
|
try:
|
||||||
|
date_str = re.search(r"\s+%s\s*:\s+(.*)" % name, out_text).group(1)
|
||||||
|
# For some reason Python's strptime() does not return any timezone information,
|
||||||
|
# even though the information is there and a supported timezone for all supported
|
||||||
|
# Python implementations (GMT). So we have to modify the datetime object by
|
||||||
|
# replacing it by UTC.
|
||||||
|
return ensure_utc_timezone(datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z'))
|
||||||
|
except AttributeError:
|
||||||
|
raise BackendException("No '{0}' date found{1}".format(name, cert_filename_suffix))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise BackendException("Failed to parse '{0}' date{1}: {2}".format(name, cert_filename_suffix, exc))
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_octets(octets_text):
|
||||||
|
return binascii.unhexlify(re.sub(r"(\s|:)", "", octets_text).encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_octets(out_text, name, required=True, potential_prefixes=None):
|
||||||
|
regexp = r"\s+%s:\s*\n\s+%s([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2})*)\s*\n" % (
|
||||||
|
name,
|
||||||
|
('(?:%s)' % '|'.join(re.escape(pp) for pp in potential_prefixes)) if potential_prefixes else '',
|
||||||
|
)
|
||||||
|
match = re.search(regexp, out_text, re.MULTILINE | re.DOTALL)
|
||||||
|
if match is not None:
|
||||||
|
return _decode_octets(match.group(1))
|
||||||
|
if not required:
|
||||||
|
return None
|
||||||
|
raise BackendException("No '{0}' octet string found".format(name))
|
||||||
|
|
||||||
|
|
||||||
|
class OpenSSLCLIBackend(CryptoBackend):
|
||||||
|
def __init__(self, module, openssl_binary=None):
|
||||||
|
super(OpenSSLCLIBackend, self).__init__(module, with_timezone=True)
|
||||||
|
if openssl_binary is None:
|
||||||
|
openssl_binary = module.get_bin_path('openssl', True)
|
||||||
|
self.openssl_binary = openssl_binary
|
||||||
|
|
||||||
|
def parse_key(self, key_file=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 is not given, but key_content, write that to a temporary file
|
||||||
|
if key_file is None:
|
||||||
|
fd, tmpsrc = tempfile.mkstemp()
|
||||||
|
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
||||||
|
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"]
|
||||||
|
rc, out, err = self.module.run_command(
|
||||||
|
openssl_keydump_cmd, check_rc=False, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||||
|
if rc != 0:
|
||||||
|
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_keydump_cmd), stderr=to_text(err)))
|
||||||
|
|
||||||
|
out_text = to_text(out, errors='surrogate_or_strict')
|
||||||
|
|
||||||
|
if account_key_type == 'rsa':
|
||||||
|
pub_hex = re.search(r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent", out_text, re.MULTILINE | re.DOTALL).group(1)
|
||||||
|
|
||||||
|
pub_exp = re.search(r"\npublicExponent: ([0-9]+)", out_text, re.MULTILINE | re.DOTALL).group(1)
|
||||||
|
pub_exp = "{0:x}".format(int(pub_exp))
|
||||||
|
if len(pub_exp) % 2:
|
||||||
|
pub_exp = "0{0}".format(pub_exp)
|
||||||
|
|
||||||
|
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(_decode_octets(pub_hex)),
|
||||||
|
},
|
||||||
|
'hash': 'sha256',
|
||||||
|
}
|
||||||
|
elif account_key_type == 'ec':
|
||||||
|
pub_data = re.search(
|
||||||
|
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
|
||||||
|
out_text,
|
||||||
|
re.MULTILINE | re.DOTALL,
|
||||||
|
)
|
||||||
|
if pub_data is None:
|
||||||
|
raise KeyParsingError('cannot parse elliptic curve key')
|
||||||
|
pub_hex = _decode_octets(pub_data.group(1))
|
||||||
|
asn1_oid_curve = pub_data.group(2).lower()
|
||||||
|
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
|
||||||
|
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
|
||||||
|
bits = 256
|
||||||
|
alg = 'ES256'
|
||||||
|
hashalg = 'sha256'
|
||||||
|
point_size = 32
|
||||||
|
curve = 'P-256'
|
||||||
|
elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
|
||||||
|
bits = 384
|
||||||
|
alg = 'ES384'
|
||||||
|
hashalg = 'sha384'
|
||||||
|
point_size = 48
|
||||||
|
curve = 'P-384'
|
||||||
|
elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
|
||||||
|
# Not yet supported on Let's Encrypt side, see
|
||||||
|
# https://github.com/letsencrypt/boulder/issues/2217
|
||||||
|
bits = 521
|
||||||
|
alg = 'ES512'
|
||||||
|
hashalg = 'sha512'
|
||||||
|
point_size = 66
|
||||||
|
curve = 'P-521'
|
||||||
|
else:
|
||||||
|
raise KeyParsingError('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
|
||||||
|
|
||||||
|
rc, out, err = self.module.run_command(
|
||||||
|
openssl_sign_cmd, data=sign_payload, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||||
|
if rc != 0:
|
||||||
|
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_sign_cmd), stderr=to_text(err)))
|
||||||
|
|
||||||
|
if key_data['type'] == 'ec':
|
||||||
|
dummy, der_out, dummy = self.module.run_command(
|
||||||
|
[self.openssl_binary, "asn1parse", "-inform", "DER"],
|
||||||
|
data=out, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||||
|
expected_len = 2 * key_data['point_size']
|
||||||
|
sig = re.findall(
|
||||||
|
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(ipaddress.ip_address(to_text(ip)).compressed)
|
||||||
|
except ValueError:
|
||||||
|
# We do not want to error out on something IPAddress() cannot parse
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
|
||||||
|
'''
|
||||||
|
Return a list of requested identifiers (CN and SANs) for the CSR.
|
||||||
|
Each identifier is a pair (type, identifier), where type is either
|
||||||
|
'dns' or 'ip'.
|
||||||
|
|
||||||
|
The list is deduplicated, and if a CNAME is present, it will be returned
|
||||||
|
as the first element in the result.
|
||||||
|
'''
|
||||||
|
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"]
|
||||||
|
rc, out, err = self.module.run_command(
|
||||||
|
openssl_csr_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||||
|
if rc != 0:
|
||||||
|
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_csr_cmd), stderr=to_text(err)))
|
||||||
|
|
||||||
|
identifiers = set()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def add_identifier(identifier):
|
||||||
|
if identifier in identifiers:
|
||||||
|
return
|
||||||
|
identifiers.add(identifier)
|
||||||
|
result.append(identifier)
|
||||||
|
|
||||||
|
common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
|
||||||
|
if common_name is not None:
|
||||||
|
add_identifier(('dns', common_name.group(1)))
|
||||||
|
subject_alt_names = re.search(
|
||||||
|
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
|
||||||
|
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
|
||||||
|
if subject_alt_names is not None:
|
||||||
|
for san in subject_alt_names.group(1).split(", "):
|
||||||
|
if san.lower().startswith("dns:"):
|
||||||
|
add_identifier(('dns', san[4:]))
|
||||||
|
elif san.lower().startswith("ip:"):
|
||||||
|
add_identifier(('ip', self._normalize_ip(san[3:])))
|
||||||
|
elif san.lower().startswith("ip address:"):
|
||||||
|
add_identifier(('ip', self._normalize_ip(san[11:])))
|
||||||
|
else:
|
||||||
|
raise BackendException('Found unsupported SAN identifier "{0}"'.format(san))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
|
||||||
|
'''
|
||||||
|
Return a set of requested identifiers (CN and SANs) for the CSR.
|
||||||
|
Each identifier is a pair (type, identifier), where type is either
|
||||||
|
'dns' or 'ip'.
|
||||||
|
'''
|
||||||
|
return set(self.get_ordered_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
|
||||||
|
|
||||||
|
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
|
||||||
|
'''
|
||||||
|
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"]
|
||||||
|
rc, out, err = self.module.run_command(
|
||||||
|
openssl_cert_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||||
|
if rc != 0:
|
||||||
|
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_cert_cmd), stderr=to_text(err)))
|
||||||
|
|
||||||
|
out_text = to_text(out, errors='surrogate_or_strict')
|
||||||
|
not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix)
|
||||||
|
if now is None:
|
||||||
|
now = self.get_now()
|
||||||
|
else:
|
||||||
|
now = ensure_utc_timezone(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.')
|
||||||
|
|
||||||
|
def get_cert_information(self, cert_filename=None, cert_content=None):
|
||||||
|
'''
|
||||||
|
Return some information on a X.509 certificate as a CertificateInformation object.
|
||||||
|
'''
|
||||||
|
filename = cert_filename
|
||||||
|
data = None
|
||||||
|
if cert_filename is not None:
|
||||||
|
cert_filename_suffix = ' in {0}'.format(cert_filename)
|
||||||
|
else:
|
||||||
|
filename = '/dev/stdin'
|
||||||
|
data = to_bytes(cert_content)
|
||||||
|
cert_filename_suffix = ''
|
||||||
|
|
||||||
|
openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"]
|
||||||
|
rc, out, err = self.module.run_command(
|
||||||
|
openssl_cert_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
|
||||||
|
if rc != 0:
|
||||||
|
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_cert_cmd), stderr=to_text(err)))
|
||||||
|
|
||||||
|
out_text = to_text(out, errors='surrogate_or_strict')
|
||||||
|
|
||||||
|
not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix)
|
||||||
|
not_before = _extract_date(out_text, 'Not Before', cert_filename_suffix=cert_filename_suffix)
|
||||||
|
|
||||||
|
sn = re.search(
|
||||||
|
r" Serial Number: ([0-9]+)",
|
||||||
|
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
|
||||||
|
if sn:
|
||||||
|
serial = int(sn.group(1))
|
||||||
|
else:
|
||||||
|
serial = convert_bytes_to_int(_extract_octets(out_text, 'Serial Number', required=True))
|
||||||
|
|
||||||
|
ski = _extract_octets(out_text, 'X509v3 Subject Key Identifier', required=False)
|
||||||
|
aki = _extract_octets(out_text, 'X509v3 Authority Key Identifier', required=False, potential_prefixes=['keyid:', ''])
|
||||||
|
|
||||||
|
return CertificateInformation(
|
||||||
|
not_valid_after=not_after,
|
||||||
|
not_valid_before=not_before,
|
||||||
|
serial_number=serial,
|
||||||
|
subject_key_identifier=ski,
|
||||||
|
authority_key_identifier=aki,
|
||||||
|
)
|
||||||
183
plugins/module_utils/acme/backends.py
Normal file
183
plugins/module_utils/acme/backends.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
import abc
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ansible.module_utils import six
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||||
|
BackendException,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import (
|
||||||
|
ensure_utc_timezone,
|
||||||
|
from_epoch_seconds,
|
||||||
|
get_epoch_seconds,
|
||||||
|
get_now_datetime,
|
||||||
|
get_relative_time_option,
|
||||||
|
remove_timezone,
|
||||||
|
UTC,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CertificateInformation = namedtuple(
|
||||||
|
'CertificateInformation',
|
||||||
|
(
|
||||||
|
'not_valid_after',
|
||||||
|
'not_valid_before',
|
||||||
|
'serial_number',
|
||||||
|
'subject_key_identifier',
|
||||||
|
'authority_key_identifier',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_FRACTIONAL_MATCHER = re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(|\.\d+)(Z|[+-]\d{2}:?\d{2}.*)$')
|
||||||
|
|
||||||
|
|
||||||
|
def _reduce_fractional_digits(timestamp_str):
|
||||||
|
"""
|
||||||
|
Given a RFC 3339 timestamp that includes too many digits for the fractional seconds part, reduces these to at most 6.
|
||||||
|
"""
|
||||||
|
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
|
||||||
|
m = _FRACTIONAL_MATCHER.match(timestamp_str)
|
||||||
|
if not m:
|
||||||
|
raise BackendException('Cannot parse ISO 8601 timestamp {0!r}'.format(timestamp_str))
|
||||||
|
timestamp, fractional, timezone = m.groups()
|
||||||
|
if len(fractional) > 7:
|
||||||
|
# Python does not support anything smaller than microseconds
|
||||||
|
# (Golang supports nanoseconds, Boulder often emits more fractional digits, which Python chokes on)
|
||||||
|
fractional = fractional[:7]
|
||||||
|
return '%s%s%s' % (timestamp, fractional, timezone)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_acme_timestamp(timestamp_str, with_timezone):
|
||||||
|
"""
|
||||||
|
Parses a RFC 3339 timestamp.
|
||||||
|
"""
|
||||||
|
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
|
||||||
|
timestamp_str = _reduce_fractional_digits(timestamp_str)
|
||||||
|
for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S.%f%z'):
|
||||||
|
# Note that %z will not work with Python 2... https://stackoverflow.com/a/27829491
|
||||||
|
try:
|
||||||
|
result = datetime.datetime.strptime(timestamp_str, format)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return ensure_utc_timezone(result) if with_timezone else remove_timezone(result)
|
||||||
|
raise BackendException('Cannot parse ISO 8601 timestamp {0!r}'.format(timestamp_str))
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class CryptoBackend(object):
|
||||||
|
def __init__(self, module, with_timezone=False):
|
||||||
|
self.module = module
|
||||||
|
self._with_timezone = with_timezone
|
||||||
|
|
||||||
|
def get_now(self):
|
||||||
|
return get_now_datetime(with_timezone=self._with_timezone)
|
||||||
|
|
||||||
|
def parse_acme_timestamp(self, timestamp_str):
|
||||||
|
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
|
||||||
|
return _parse_acme_timestamp(timestamp_str, with_timezone=self._with_timezone)
|
||||||
|
|
||||||
|
def parse_module_parameter(self, value, name):
|
||||||
|
try:
|
||||||
|
return get_relative_time_option(value, name, backend='cryptography', with_timezone=self._with_timezone)
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise BackendException(to_native(exc))
|
||||||
|
|
||||||
|
def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage):
|
||||||
|
start = get_epoch_seconds(timestamp_start)
|
||||||
|
end = get_epoch_seconds(timestamp_end)
|
||||||
|
return from_epoch_seconds(start + percentage * (end - start), with_timezone=self._with_timezone)
|
||||||
|
|
||||||
|
def get_utc_datetime(self, *args, **kwargs):
|
||||||
|
kwargs_ext = dict(kwargs)
|
||||||
|
if self._with_timezone and ('tzinfo' not in kwargs_ext and len(args) < 8):
|
||||||
|
kwargs_ext['tzinfo'] = UTC
|
||||||
|
result = datetime.datetime(*args, **kwargs_ext)
|
||||||
|
if self._with_timezone and ('tzinfo' in kwargs or len(args) >= 8):
|
||||||
|
result = ensure_utc_timezone(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def parse_key(self, key_file=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.'''
|
||||||
|
|
||||||
|
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
|
||||||
|
'''
|
||||||
|
Return a list of requested identifiers (CN and SANs) for the CSR.
|
||||||
|
Each identifier is a pair (type, identifier), where type is either
|
||||||
|
'dns' or 'ip'.
|
||||||
|
|
||||||
|
The list is deduplicated, and if a CNAME is present, it will be returned
|
||||||
|
as the first element in the result.
|
||||||
|
'''
|
||||||
|
self.module.deprecate(
|
||||||
|
"Every backend must override the get_ordered_csr_identifiers() method."
|
||||||
|
" The default implementation will be removed in 3.0.0 and this method will be marked as `abstractmethod` by then.",
|
||||||
|
version='3.0.0',
|
||||||
|
collection_name='community.crypto',
|
||||||
|
)
|
||||||
|
return sorted(self.get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
|
||||||
|
'''
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def get_cert_information(self, cert_filename=None, cert_content=None):
|
||||||
|
'''
|
||||||
|
Return some information on a X.509 certificate as a CertificateInformation object.
|
||||||
|
'''
|
||||||
|
# Not implementing this method in a backend is DEPRECATED and will be
|
||||||
|
# disallowed in community.crypto 3.0.0. This method will be marked as
|
||||||
|
# @abstractmethod by then.
|
||||||
|
raise BackendException('This backend does not support get_cert_information()')
|
||||||
280
plugins/module_utils/acme/certificate.py
Normal file
280
plugins/module_utils/acme/certificate.py
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024 Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
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 (
|
||||||
|
Authorization,
|
||||||
|
wait_for_validation,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
|
||||||
|
CertificateChain,
|
||||||
|
Criterium,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||||
|
ModuleFailException,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.orders import (
|
||||||
|
Order,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
|
||||||
|
write_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||||
|
pem_to_der,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ACMECertificateClient(object):
|
||||||
|
'''
|
||||||
|
ACME v2 client class. Uses an ACME account object and a CSR to
|
||||||
|
start and validate ACME challenges and download the respective
|
||||||
|
certificates.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, module, backend, client=None, account=None):
|
||||||
|
self.module = module
|
||||||
|
self.version = module.params['acme_version']
|
||||||
|
self.csr = module.params.get('csr')
|
||||||
|
self.csr_content = module.params.get('csr_content')
|
||||||
|
if client is None:
|
||||||
|
client = ACMEClient(module, backend)
|
||||||
|
self.client = client
|
||||||
|
if account is None:
|
||||||
|
account = ACMEAccount(self.client)
|
||||||
|
self.account = account
|
||||||
|
self.order_uri = module.params.get('order_uri')
|
||||||
|
self.order_creation_error_strategy = module.params.get('order_creation_error_strategy', 'auto')
|
||||||
|
self.order_creation_max_retries = module.params.get('order_creation_max_retries', 3)
|
||||||
|
|
||||||
|
# Make sure account exists
|
||||||
|
dummy, account_data = self.account.setup_account(allow_creation=False)
|
||||||
|
if account_data is None:
|
||||||
|
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
||||||
|
|
||||||
|
if self.csr is not None and not os.path.exists(self.csr):
|
||||||
|
raise ModuleFailException("CSR %s not found" % (self.csr))
|
||||||
|
|
||||||
|
# Extract list of identifiers from CSR
|
||||||
|
if self.csr is not None or self.csr_content is not None:
|
||||||
|
self.identifiers = self.client.backend.get_ordered_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
|
||||||
|
else:
|
||||||
|
self.identifiers = None
|
||||||
|
|
||||||
|
def parse_select_chain(self, select_chain):
|
||||||
|
select_chain_matcher = []
|
||||||
|
if select_chain:
|
||||||
|
for criterium_idx, criterium in enumerate(select_chain):
|
||||||
|
try:
|
||||||
|
select_chain_matcher.append(
|
||||||
|
self.client.backend.create_chain_matcher(Criterium(criterium, index=criterium_idx)))
|
||||||
|
except ValueError as exc:
|
||||||
|
self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc))
|
||||||
|
return select_chain_matcher
|
||||||
|
|
||||||
|
def load_order(self):
|
||||||
|
if not self.order_uri:
|
||||||
|
raise ModuleFailException('The order URI has not been provided')
|
||||||
|
order = Order.from_url(self.client, self.order_uri)
|
||||||
|
order.load_authorizations(self.client)
|
||||||
|
return order
|
||||||
|
|
||||||
|
def create_order(self, replaces_cert_id=None, profile=None):
|
||||||
|
'''
|
||||||
|
Create a new order.
|
||||||
|
'''
|
||||||
|
if self.identifiers is None:
|
||||||
|
raise ModuleFailException('No identifiers have been provided')
|
||||||
|
order = Order.create_with_error_handling(
|
||||||
|
self.client,
|
||||||
|
self.identifiers,
|
||||||
|
error_strategy=self.order_creation_error_strategy,
|
||||||
|
error_max_retries=self.order_creation_max_retries,
|
||||||
|
replaces_cert_id=replaces_cert_id,
|
||||||
|
profile=profile,
|
||||||
|
message_callback=self.module.warn,
|
||||||
|
)
|
||||||
|
self.order_uri = order.url
|
||||||
|
order.load_authorizations(self.client)
|
||||||
|
return order
|
||||||
|
|
||||||
|
def get_challenges_data(self, order):
|
||||||
|
'''
|
||||||
|
Get challenge details.
|
||||||
|
|
||||||
|
Return a tuple of generic challenge details, and specialized DNS challenge details.
|
||||||
|
'''
|
||||||
|
# Get general challenge data
|
||||||
|
data = []
|
||||||
|
for authz in order.authorizations.values():
|
||||||
|
# Skip valid authentications: their challenges are already valid
|
||||||
|
# and do not need to be returned
|
||||||
|
if authz.status == 'valid':
|
||||||
|
continue
|
||||||
|
data.append(dict(
|
||||||
|
identifier=authz.identifier,
|
||||||
|
identifier_type=authz.identifier_type,
|
||||||
|
challenges=authz.get_challenge_data(self.client),
|
||||||
|
))
|
||||||
|
# Get DNS challenge data
|
||||||
|
data_dns = {}
|
||||||
|
dns_challenge_type = 'dns-01'
|
||||||
|
for entry in data:
|
||||||
|
dns_challenge = entry['challenges'].get(dns_challenge_type)
|
||||||
|
if dns_challenge:
|
||||||
|
values = data_dns.get(dns_challenge['record'])
|
||||||
|
if values is None:
|
||||||
|
values = []
|
||||||
|
data_dns[dns_challenge['record']] = values
|
||||||
|
values.append(dns_challenge['resource_value'])
|
||||||
|
return data, data_dns
|
||||||
|
|
||||||
|
def check_that_authorizations_can_be_used(self, order):
|
||||||
|
bad_authzs = []
|
||||||
|
for authz in order.authorizations.values():
|
||||||
|
if authz.status not in ('valid', 'pending'):
|
||||||
|
bad_authzs.append('{authz} (status={status!r})'.format(
|
||||||
|
authz=authz.combined_identifier,
|
||||||
|
status=authz.status,
|
||||||
|
))
|
||||||
|
if bad_authzs:
|
||||||
|
raise ModuleFailException(
|
||||||
|
'Some of the authorizations for the order are in a bad state, so the order'
|
||||||
|
' can no longer be satisfied: {bad_authzs}'.format(
|
||||||
|
bad_authzs=', '.join(sorted(bad_authzs)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def collect_invalid_authzs(self, order):
|
||||||
|
return [authz for authz in order.authorizations.values() if authz.status == 'invalid']
|
||||||
|
|
||||||
|
def collect_pending_authzs(self, order):
|
||||||
|
return [authz for authz in order.authorizations.values() if authz.status == 'pending']
|
||||||
|
|
||||||
|
def call_validate(self, pending_authzs, get_challenge, wait=True):
|
||||||
|
authzs_with_challenges_to_wait_for = []
|
||||||
|
for authz in pending_authzs:
|
||||||
|
challenge_type = get_challenge(authz)
|
||||||
|
authz.call_validate(self.client, challenge_type, wait=wait)
|
||||||
|
authzs_with_challenges_to_wait_for.append((authz, challenge_type, authz.find_challenge(challenge_type)))
|
||||||
|
return authzs_with_challenges_to_wait_for
|
||||||
|
|
||||||
|
def wait_for_validation(self, authzs_to_wait_for):
|
||||||
|
wait_for_validation(authzs_to_wait_for, self.client)
|
||||||
|
|
||||||
|
def _download_alternate_chains(self, cert):
|
||||||
|
alternate_chains = []
|
||||||
|
for alternate in cert.alternates:
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
if alt_cert.cert is not None:
|
||||||
|
alternate_chains.append(alt_cert)
|
||||||
|
else:
|
||||||
|
self.module.warn('Error while downloading alternative certificate {0}: no certificate found'.format(alternate))
|
||||||
|
return alternate_chains
|
||||||
|
|
||||||
|
def download_certificate(self, order, download_all_chains=True):
|
||||||
|
'''
|
||||||
|
Download certificate from a valid oder.
|
||||||
|
'''
|
||||||
|
if order.status != 'valid':
|
||||||
|
raise ModuleFailException('The order must be valid, but has state {state!r}!'.format(state=order.state))
|
||||||
|
|
||||||
|
if not order.certificate_uri:
|
||||||
|
raise ModuleFailException("Order's crtificate URL {url!r} is empty!".format(url=order.certificate_uri))
|
||||||
|
|
||||||
|
cert = CertificateChain.download(self.client, order.certificate_uri)
|
||||||
|
if cert.cert is None:
|
||||||
|
raise ModuleFailException('Certificate at {url} is empty!'.format(url=order.certificate_uri))
|
||||||
|
|
||||||
|
alternate_chains = None
|
||||||
|
if download_all_chains:
|
||||||
|
alternate_chains = self._download_alternate_chains(cert)
|
||||||
|
|
||||||
|
return cert, alternate_chains
|
||||||
|
|
||||||
|
def get_certificate(self, order, download_all_chains=True):
|
||||||
|
'''
|
||||||
|
Request a new certificate and downloads it, and optionally all certificate chains.
|
||||||
|
First verifies whether all authorizations are valid; if not, aborts with an error.
|
||||||
|
'''
|
||||||
|
if self.csr is None and self.csr_content is None:
|
||||||
|
raise ModuleFailException('No CSR has been provided')
|
||||||
|
for identifier, authz in order.authorizations.items():
|
||||||
|
if authz.status != 'valid':
|
||||||
|
authz.raise_error('Status is {status!r} and not "valid"'.format(status=authz.status), module=self.module)
|
||||||
|
|
||||||
|
order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
|
||||||
|
|
||||||
|
return self.download_certificate(order, download_all_chains=download_all_chains)
|
||||||
|
|
||||||
|
def find_matching_chain(self, chains, select_chain_matcher):
|
||||||
|
for criterium_idx, matcher in enumerate(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 write_cert_chain(self, cert, cert_dest=None, fullchain_dest=None, chain_dest=None):
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if cert_dest and write_file(self.module, cert_dest, cert.cert.encode('utf8')):
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if fullchain_dest and write_file(self.module, fullchain_dest, (cert.cert + "\n".join(cert.chain)).encode('utf8')):
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if chain_dest and write_file(self.module, chain_dest, ("\n".join(cert.chain)).encode('utf8')):
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def deactivate_authzs(self, order):
|
||||||
|
'''
|
||||||
|
Deactivates all valid authz's. Does not raise exceptions.
|
||||||
|
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||||
|
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||||
|
'''
|
||||||
|
if len(order.authorization_uris) > len(order.authorizations):
|
||||||
|
for authz_uri in order.authorization_uris:
|
||||||
|
authz = None
|
||||||
|
try:
|
||||||
|
authz = Authorization.deactivate_url(self.client, authz_uri)
|
||||||
|
except Exception:
|
||||||
|
# ignore errors
|
||||||
|
pass
|
||||||
|
if authz is None or authz.status != 'deactivated':
|
||||||
|
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz_uri))
|
||||||
|
else:
|
||||||
|
for authz in order.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))
|
||||||
129
plugins/module_utils/acme/certificates.py
Normal file
129
plugins/module_utils/acme/certificates.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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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
|
||||||
358
plugins/module_utils/acme/challenges.py
Normal file
358
plugins/module_utils/acme/challenges.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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.acme.utils import (
|
||||||
|
nopad_b64,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||||
|
format_error_problem,
|
||||||
|
ACMEProtocolException,
|
||||||
|
ModuleFailException,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ipaddress
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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 normalize_combined_identifier(identifier):
|
||||||
|
identifier_type, identifier = split_identifier(identifier)
|
||||||
|
# Normalize DNS names and IPs
|
||||||
|
identifier = identifier.lower()
|
||||||
|
return combine_identifier(identifier_type, 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 = '{0}.{1}'.format(resource, identifier[2:] if identifier.startswith('*.') else identifier)
|
||||||
|
return {
|
||||||
|
'resource': resource,
|
||||||
|
'resource_value': value,
|
||||||
|
'record': record,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.type == 'tls-alpn-01':
|
||||||
|
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3
|
||||||
|
if identifier_type == 'ip':
|
||||||
|
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
|
||||||
|
resource = ipaddress.ip_address(identifier).reverse_pointer
|
||||||
|
if not resource.endswith('.'):
|
||||||
|
resource += '.'
|
||||||
|
else:
|
||||||
|
resource = identifier
|
||||||
|
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
|
||||||
|
# While 'challenges' is a required field, apparently not every CA cares
|
||||||
|
# (https://github.com/ansible-collections/community.crypto/issues/824)
|
||||||
|
if data.get('challenges'):
|
||||||
|
self.challenges = [Challenge.from_json(client, challenge) for challenge in data['challenges']]
|
||||||
|
else:
|
||||||
|
self.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 can_deactivate(self):
|
||||||
|
'''
|
||||||
|
Deactivates this authorization.
|
||||||
|
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||||
|
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||||
|
'''
|
||||||
|
return self.status in ('valid', 'pending')
|
||||||
|
|
||||||
|
def deactivate(self, client):
|
||||||
|
'''
|
||||||
|
Deactivates this authorization.
|
||||||
|
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||||
|
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||||
|
'''
|
||||||
|
if not self.can_deactivate():
|
||||||
|
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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deactivate_url(cls, client, url):
|
||||||
|
'''
|
||||||
|
Deactivates this authorization.
|
||||||
|
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||||
|
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||||
|
'''
|
||||||
|
authz = cls(url)
|
||||||
|
authz_deactivate = {
|
||||||
|
'status': 'deactivated'
|
||||||
|
}
|
||||||
|
if client.version == 1:
|
||||||
|
authz_deactivate['resource'] = 'authz'
|
||||||
|
result, info = client.send_signed_request(url, authz_deactivate, fail_on_error=True)
|
||||||
|
authz._setup(client, result)
|
||||||
|
return authz
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_validation(authzs, client):
|
||||||
|
'''
|
||||||
|
Wait until a list of authz is valid. Fail if at least one of them is invalid or revoked.
|
||||||
|
'''
|
||||||
|
while authzs:
|
||||||
|
authzs_next = []
|
||||||
|
for authz in authzs:
|
||||||
|
authz.refresh(client)
|
||||||
|
if authz.status in ['valid', 'invalid', 'revoked']:
|
||||||
|
if authz.status != 'valid':
|
||||||
|
authz.raise_error('Status is not "valid"', module=client.module)
|
||||||
|
else:
|
||||||
|
authzs_next.append(authz)
|
||||||
|
if authzs_next:
|
||||||
|
time.sleep(2)
|
||||||
|
authzs = authzs_next
|
||||||
156
plugins/module_utils/acme/errors.py
Normal file
156
plugins/module_utils/acme/errors.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from ansible.module_utils.common.text.converters import to_text
|
||||||
|
from ansible.module_utils.six import binary_type, PY3
|
||||||
|
from ansible.module_utils.six.moves.http_client import responses as http_responses
|
||||||
|
|
||||||
|
|
||||||
|
def format_http_status(status_code):
|
||||||
|
expl = http_responses.get(status_code)
|
||||||
|
if not expl:
|
||||||
|
return str(status_code)
|
||||||
|
return '%d %s' % (status_code, expl)
|
||||||
|
|
||||||
|
|
||||||
|
def format_error_problem(problem, subproblem_prefix=''):
|
||||||
|
error_type = problem.get('type', 'about:blank') # https://www.rfc-editor.org/rfc/rfc7807#section-3.1
|
||||||
|
if 'title' in problem:
|
||||||
|
msg = 'Error "{title}" ({type})'.format(
|
||||||
|
type=error_type,
|
||||||
|
title=problem['title'],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = 'Error {type}'.format(type=error_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()
|
||||||
|
error_code = None
|
||||||
|
error_type = None
|
||||||
|
|
||||||
|
if msg is None:
|
||||||
|
msg = 'ACME request failed'
|
||||||
|
add_msg = ''
|
||||||
|
|
||||||
|
if info is not None:
|
||||||
|
url = info['url']
|
||||||
|
code = info['status']
|
||||||
|
extras['http_url'] = url
|
||||||
|
extras['http_status'] = code
|
||||||
|
error_code = code
|
||||||
|
if code is not None and code >= 400 and content_json is not None and 'type' in content_json:
|
||||||
|
error_type = content_json['type']
|
||||||
|
if 'status' in content_json and content_json['status'] != code:
|
||||||
|
code_msg = 'status {problem_code} (HTTP status: {http_code})'.format(
|
||||||
|
http_code=format_http_status(code), problem_code=content_json['status'])
|
||||||
|
else:
|
||||||
|
code_msg = 'status {problem_code}'.format(problem_code=format_http_status(code))
|
||||||
|
if code == -1 and info.get('msg'):
|
||||||
|
code_msg = 'error: {msg}'.format(msg=info['msg'])
|
||||||
|
subproblems = content_json.pop('subproblems', None)
|
||||||
|
add_msg = ' {problem}.'.format(problem=format_error_problem(content_json))
|
||||||
|
extras['problem'] = content_json
|
||||||
|
extras['subproblems'] = subproblems or []
|
||||||
|
if subproblems is not None:
|
||||||
|
add_msg = '{add_msg} Subproblems:'.format(add_msg=add_msg)
|
||||||
|
for index, problem in enumerate(subproblems):
|
||||||
|
add_msg = '{add_msg}\n({index}) {problem}.'.format(
|
||||||
|
add_msg=add_msg,
|
||||||
|
index=index,
|
||||||
|
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
code_msg = 'HTTP status {code}'.format(code=format_http_status(code))
|
||||||
|
if code == -1 and info.get('msg'):
|
||||||
|
code_msg = 'error: {msg}'.format(msg=info['msg'])
|
||||||
|
if content_json is not None:
|
||||||
|
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_msg)
|
||||||
|
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 = []
|
||||||
|
self.error_code = error_code
|
||||||
|
self.error_type = error_type
|
||||||
|
for k, v in extras.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
|
||||||
|
class BackendException(ModuleFailException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkException(ModuleFailException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeyParsingError(ModuleFailException):
|
||||||
|
pass
|
||||||
87
plugins/module_utils/acme/io.py
Normal file
87
plugins/module_utils/acme/io.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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
|
||||||
187
plugins/module_utils/acme/orders.py
Normal file
187
plugins/module_utils/acme/orders.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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,
|
||||||
|
normalize_combined_identifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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.replaces_cert_id = data.get('replaces')
|
||||||
|
self.finalize_uri = data.get('finalize')
|
||||||
|
self.certificate_uri = data.get('certificate')
|
||||||
|
self.authorization_uris = data['authorizations']
|
||||||
|
self.authorizations = {}
|
||||||
|
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
self.status = None
|
||||||
|
self.identifiers = []
|
||||||
|
self.replaces_cert_id = None
|
||||||
|
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, replaces_cert_id=None, profile=None):
|
||||||
|
'''
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if replaces_cert_id is not None:
|
||||||
|
new_order["replaces"] = replaces_cert_id
|
||||||
|
if profile is not None:
|
||||||
|
new_order["profile"] = profile
|
||||||
|
result, info = client.send_signed_request(
|
||||||
|
client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201])
|
||||||
|
return cls.from_json(client, result, info['location'])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_with_error_handling(
|
||||||
|
cls,
|
||||||
|
client,
|
||||||
|
identifiers,
|
||||||
|
error_strategy='auto',
|
||||||
|
error_max_retries=3,
|
||||||
|
replaces_cert_id=None,
|
||||||
|
profile=None,
|
||||||
|
message_callback=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
error_strategy can be one of the following strings:
|
||||||
|
|
||||||
|
* ``fail``: simply fail. (Same behavior as ``Order.create()``.)
|
||||||
|
* ``retry_without_replaces_cert_id``: if ``replaces_cert_id`` is not ``None``, set it to ``None`` and retry.
|
||||||
|
The only exception is an error of type ``urn:ietf:params:acme:error:alreadyReplaced``, that indicates that
|
||||||
|
the certificate was already replaced.
|
||||||
|
* ``auto``: try to be clever. Right now this is identical to ``retry_without_replaces_cert_id``, but that can
|
||||||
|
change at any time in the future.
|
||||||
|
* ``always``: always retry until ``error_max_retries`` has been reached.
|
||||||
|
"""
|
||||||
|
tries = 0
|
||||||
|
while True:
|
||||||
|
tries += 1
|
||||||
|
try:
|
||||||
|
return cls.create(client, identifiers, replaces_cert_id=replaces_cert_id, profile=profile)
|
||||||
|
except ACMEProtocolException as exc:
|
||||||
|
if tries <= error_max_retries + 1 and error_strategy != 'fail':
|
||||||
|
if error_strategy == 'always':
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
error_strategy in ('auto', 'retry_without_replaces_cert_id') and
|
||||||
|
replaces_cert_id is not None and
|
||||||
|
not (exc.error_code == 409 and exc.error_type == 'urn:ietf:params:acme:error:alreadyReplaced')
|
||||||
|
):
|
||||||
|
if message_callback:
|
||||||
|
message_callback(
|
||||||
|
'Stop passing `replaces={replaces}` due to error {code} {type} when creating ACME order'.format(
|
||||||
|
code=exc.error_code,
|
||||||
|
type=exc.error_type,
|
||||||
|
replaces=replaces_cert_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
replaces_cert_id = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
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[normalize_combined_identifier(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)
|
||||||
130
plugins/module_utils/acme/utils.py
Normal file
130
plugins/module_utils/acme/utils.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
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
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import convert_int_to_bytes
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import get_now_datetime
|
||||||
|
|
||||||
|
|
||||||
|
def nopad_b64(data):
|
||||||
|
return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "")
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
|
||||||
|
'''
|
||||||
|
if 'link' in info:
|
||||||
|
link = info['link']
|
||||||
|
for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
|
||||||
|
callback(unquote(url), relation)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_retry_after(value, relative_with_timezone=True, now=None):
|
||||||
|
'''
|
||||||
|
Parse the value of a Retry-After header and return a timestamp.
|
||||||
|
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
|
'''
|
||||||
|
# First try a number of seconds
|
||||||
|
try:
|
||||||
|
delta = datetime.timedelta(seconds=int(value))
|
||||||
|
if now is None:
|
||||||
|
now = get_now_datetime(relative_with_timezone)
|
||||||
|
return now + delta
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(value, '%a, %d %b %Y %H:%M:%S GMT')
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ValueError('Cannot parse Retry-After header value %s' % repr(value))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_cert_id(
|
||||||
|
backend,
|
||||||
|
cert_info=None,
|
||||||
|
cert_filename=None,
|
||||||
|
cert_content=None,
|
||||||
|
none_if_required_information_is_missing=False,
|
||||||
|
):
|
||||||
|
# Obtain certificate info if not provided
|
||||||
|
if cert_info is None:
|
||||||
|
cert_info = backend.get_cert_information(cert_filename=cert_filename, cert_content=cert_content)
|
||||||
|
|
||||||
|
# Convert Authority Key Identifier to string
|
||||||
|
if cert_info.authority_key_identifier is None:
|
||||||
|
if none_if_required_information_is_missing:
|
||||||
|
return None
|
||||||
|
raise ModuleFailException('Certificate has no Authority Key Identifier extension')
|
||||||
|
aki = to_native(base64.urlsafe_b64encode(cert_info.authority_key_identifier)).replace('=', '')
|
||||||
|
|
||||||
|
# Convert serial number to string
|
||||||
|
serial_bytes = convert_int_to_bytes(cert_info.serial_number)
|
||||||
|
if ord(serial_bytes[:1]) >= 128:
|
||||||
|
serial_bytes = b'\x00' + serial_bytes
|
||||||
|
serial = to_native(base64.urlsafe_b64encode(serial_bytes)).replace('=', '')
|
||||||
|
|
||||||
|
# Compose cert ID
|
||||||
|
return '{aki}.{serial}'.format(aki=aki, serial=serial)
|
||||||
75
plugins/module_utils/argspec.py
Normal file
75
plugins/module_utils/argspec.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_list(value):
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
return list(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ArgumentSpec:
|
||||||
|
def __init__(self, argument_spec=None, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
|
||||||
|
self.argument_spec = argument_spec or {}
|
||||||
|
self.mutually_exclusive = _ensure_list(mutually_exclusive)
|
||||||
|
self.required_together = _ensure_list(required_together)
|
||||||
|
self.required_one_of = _ensure_list(required_one_of)
|
||||||
|
self.required_if = _ensure_list(required_if)
|
||||||
|
self.required_by = required_by or {}
|
||||||
|
|
||||||
|
def update_argspec(self, **kwargs):
|
||||||
|
self.argument_spec.update(kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def update(self, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
|
||||||
|
if mutually_exclusive:
|
||||||
|
self.mutually_exclusive.extend(mutually_exclusive)
|
||||||
|
if required_together:
|
||||||
|
self.required_together.extend(required_together)
|
||||||
|
if required_one_of:
|
||||||
|
self.required_one_of.extend(required_one_of)
|
||||||
|
if required_if:
|
||||||
|
self.required_if.extend(required_if)
|
||||||
|
if required_by:
|
||||||
|
for k, v in required_by.items():
|
||||||
|
if k in self.required_by:
|
||||||
|
v = list(self.required_by[k]) + list(v)
|
||||||
|
self.required_by[k] = v
|
||||||
|
return self
|
||||||
|
|
||||||
|
def merge(self, other):
|
||||||
|
self.update_argspec(**other.argument_spec)
|
||||||
|
self.update(
|
||||||
|
mutually_exclusive=other.mutually_exclusive,
|
||||||
|
required_together=other.required_together,
|
||||||
|
required_one_of=other.required_one_of,
|
||||||
|
required_if=other.required_if,
|
||||||
|
required_by=other.required_by,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def create_ansible_module_helper(self, clazz, args, **kwargs):
|
||||||
|
return clazz(
|
||||||
|
*args,
|
||||||
|
argument_spec=self.argument_spec,
|
||||||
|
mutually_exclusive=self.mutually_exclusive,
|
||||||
|
required_together=self.required_together,
|
||||||
|
required_one_of=self.required_one_of,
|
||||||
|
required_if=self.required_if,
|
||||||
|
required_by=self.required_by,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def create_ansible_module(self, **kwargs):
|
||||||
|
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('ArgumentSpec', )
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,99 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
# THIS FILE IS FOR COMPATIBILITY ONLY! YOU SHALL NOT IMPORT IT!
|
|
||||||
#
|
|
||||||
# This fill will be removed eventually, so if you're using it,
|
|
||||||
# please stop doing so.
|
|
||||||
|
|
||||||
from .basic import (
|
|
||||||
HAS_PYOPENSSL,
|
|
||||||
CRYPTOGRAPHY_HAS_X25519,
|
|
||||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
|
||||||
CRYPTOGRAPHY_HAS_X448,
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519,
|
|
||||||
CRYPTOGRAPHY_HAS_ED448,
|
|
||||||
HAS_CRYPTOGRAPHY,
|
|
||||||
OpenSSLObjectError,
|
|
||||||
OpenSSLBadPassphraseError,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .cryptography_crl import (
|
|
||||||
REVOCATION_REASON_MAP,
|
|
||||||
REVOCATION_REASON_MAP_INVERSE,
|
|
||||||
cryptography_decode_revoked_certificate,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .cryptography_support import (
|
|
||||||
cryptography_get_extensions_from_cert,
|
|
||||||
cryptography_get_extensions_from_csr,
|
|
||||||
cryptography_name_to_oid,
|
|
||||||
cryptography_oid_to_name,
|
|
||||||
cryptography_get_name,
|
|
||||||
cryptography_decode_name,
|
|
||||||
cryptography_parse_key_usage_params,
|
|
||||||
cryptography_get_basic_constraints,
|
|
||||||
cryptography_key_needs_digest_for_signing,
|
|
||||||
cryptography_compare_public_keys,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .identify import (
|
|
||||||
identify_private_key_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .math import (
|
|
||||||
binary_exp_mod,
|
|
||||||
simple_gcd,
|
|
||||||
quick_is_not_prime,
|
|
||||||
count_bits,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ._obj2txt import obj2txt as _obj2txt
|
|
||||||
|
|
||||||
from ._objects_data import OID_MAP as _OID_MAP
|
|
||||||
|
|
||||||
from ._objects import OID_LOOKUP as _OID_LOOKUP
|
|
||||||
from ._objects import NORMALIZE_NAMES as _NORMALIZE_NAMES
|
|
||||||
from ._objects import NORMALIZE_NAMES_SHORT as _NORMALIZE_NAMES_SHORT
|
|
||||||
|
|
||||||
from .pyopenssl_support import (
|
|
||||||
pyopenssl_normalize_name,
|
|
||||||
pyopenssl_get_extensions_from_cert,
|
|
||||||
pyopenssl_get_extensions_from_csr,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .support import (
|
|
||||||
get_fingerprint_of_bytes,
|
|
||||||
get_fingerprint,
|
|
||||||
load_privatekey,
|
|
||||||
load_certificate,
|
|
||||||
load_certificate_request,
|
|
||||||
parse_name_field,
|
|
||||||
convert_relative_to_datetime,
|
|
||||||
get_relative_time_option,
|
|
||||||
select_message_digest,
|
|
||||||
OpenSSLObject,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..io import (
|
|
||||||
load_file_if_exists,
|
|
||||||
write_file,
|
|
||||||
)
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# (c) 2020, Jordan Borean <jborean93@gmail.com>
|
# Copyright (c) 2020, Jordan Borean <jborean93@gmail.com>
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ansible.module_utils._text import to_bytes
|
from ansible.module_utils.common.text.converters import to_bytes
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
# This code is part of Ansible, but is an independent component.
|
||||||
|
# This particular file snippet, and this file snippet only, is licensed under the
|
||||||
|
# Apache 2.0 License. Modules you write using this snippet, which is embedded
|
||||||
|
# dynamically by Ansible, still belong to the author of the module, and may assign
|
||||||
|
# their own license to the complete work.
|
||||||
|
|
||||||
# This excerpt is dual licensed under the terms of the Apache License, Version
|
# This excerpt is dual licensed under the terms of the Apache License, Version
|
||||||
# 2.0, and the BSD License. See the LICENSE file at
|
# 2.0, and the BSD License. See the LICENSE file at
|
||||||
# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details.
|
# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details.
|
||||||
#
|
#
|
||||||
|
# The Apache 2.0 license has been included as LICENSES/Apache-2.0.txt in this collection.
|
||||||
|
# The BSD License license has been included as LICENSES/BSD-3-Clause.txt in this collection.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
|
||||||
|
#
|
||||||
# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py
|
# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py
|
||||||
#
|
#
|
||||||
# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk)
|
# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk)
|
||||||
@@ -20,6 +30,10 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__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):
|
def obj2txt(openssl_lib, openssl_ffi, obj):
|
||||||
# Set to 80 on the recommendation of
|
# Set to 80 on the recommendation of
|
||||||
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
|
||||||
#
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# 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
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
# This code is part of Ansible, but is an independent component.
|
||||||
|
# This particular file snippet, and this file snippet only, is licensed under the
|
||||||
|
# Apache 2.0 License. Modules you write using this snippet, which is embedded
|
||||||
|
# dynamically by Ansible, still belong to the author of the module, and may assign
|
||||||
|
# their own license to the complete work.
|
||||||
|
|
||||||
# This has been extracted from the OpenSSL project's objects.txt:
|
# This has been extracted from the OpenSSL project's objects.txt:
|
||||||
# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt
|
# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt
|
||||||
# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376
|
# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376
|
||||||
@@ -5,7 +11,8 @@
|
|||||||
# In case the following data structure has any copyrightable content, note that it is licensed as follows:
|
# In case the following data structure has any copyrightable content, note that it is licensed as follows:
|
||||||
# Copyright (c) the OpenSSL contributors
|
# Copyright (c) the OpenSSL contributors
|
||||||
# Licensed under the Apache License 2.0
|
# Licensed under the Apache License 2.0
|
||||||
# https://github.com/openssl/openssl/blob/master/LICENSE
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# https://github.com/openssl/openssl/blob/master/LICENSE.txt or LICENSES/Apache-2.0.txt
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
# (c) 2020, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
#
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# 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
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__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:
|
|
||||||
# Error handled in the calling module.
|
|
||||||
HAS_PYOPENSSL = False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cryptography
|
import cryptography
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
|
||||||
#
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# 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
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion as _LooseVersion
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import cryptography
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Error handled in the calling module.
|
# Error handled in the calling module.
|
||||||
@@ -30,6 +22,7 @@ from .basic import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .cryptography_support import (
|
from .cryptography_support import (
|
||||||
|
CRYPTOGRAPHY_TIMEZONE,
|
||||||
cryptography_decode_name,
|
cryptography_decode_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,6 +31,13 @@ from ._obj2txt import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: once cryptography has a _utc variant of InvalidityDate.invalidity_date, set this
|
||||||
|
# to True and adjust get_invalidity_date() accordingly.
|
||||||
|
# (https://github.com/pyca/cryptography/issues/10818)
|
||||||
|
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = False
|
||||||
|
if HAS_CRYPTOGRAPHY:
|
||||||
|
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = _LooseVersion(cryptography.__version__) >= _LooseVersion('43.0.0')
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ else:
|
|||||||
def cryptography_decode_revoked_certificate(cert):
|
def cryptography_decode_revoked_certificate(cert):
|
||||||
result = {
|
result = {
|
||||||
'serial_number': cert.serial_number,
|
'serial_number': cert.serial_number,
|
||||||
'revocation_date': cert.revocation_date,
|
'revocation_date': get_revocation_date(cert),
|
||||||
'issuer': None,
|
'issuer': None,
|
||||||
'issuer_critical': False,
|
'issuer_critical': False,
|
||||||
'reason': None,
|
'reason': None,
|
||||||
@@ -88,19 +88,19 @@ def cryptography_decode_revoked_certificate(cert):
|
|||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
ext = cert.extensions.get_extension_for_class(x509.InvalidityDate)
|
ext = cert.extensions.get_extension_for_class(x509.InvalidityDate)
|
||||||
result['invalidity_date'] = ext.value.invalidity_date
|
result['invalidity_date'] = get_invalidity_date(ext.value)
|
||||||
result['invalidity_date_critical'] = ext.critical
|
result['invalidity_date_critical'] = ext.critical
|
||||||
except x509.ExtensionNotFound:
|
except x509.ExtensionNotFound:
|
||||||
pass
|
pass
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def cryptography_dump_revoked(entry):
|
def cryptography_dump_revoked(entry, idn_rewrite='ignore'):
|
||||||
return {
|
return {
|
||||||
'serial_number': entry['serial_number'],
|
'serial_number': entry['serial_number'],
|
||||||
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
||||||
'issuer':
|
'issuer':
|
||||||
[cryptography_decode_name(issuer) for issuer in entry['issuer']]
|
[cryptography_decode_name(issuer, idn_rewrite=idn_rewrite) for issuer in entry['issuer']]
|
||||||
if entry['issuer'] is not None else None,
|
if entry['issuer'] is not None else None,
|
||||||
'issuer_critical': entry['issuer_critical'],
|
'issuer_critical': entry['issuer_critical'],
|
||||||
'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
||||||
@@ -116,10 +116,46 @@ def cryptography_get_signature_algorithm_oid_from_crl(crl):
|
|||||||
try:
|
try:
|
||||||
return crl.signature_algorithm_oid
|
return crl.signature_algorithm_oid
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Older cryptography versions don't have signature_algorithm_oid yet
|
# Older cryptography versions do not have signature_algorithm_oid yet
|
||||||
dotted = obj2txt(
|
dotted = obj2txt(
|
||||||
crl._backend._lib,
|
crl._backend._lib,
|
||||||
crl._backend._ffi,
|
crl._backend._ffi,
|
||||||
crl._x509_crl.sig_alg.algorithm
|
crl._x509_crl.sig_alg.algorithm
|
||||||
)
|
)
|
||||||
return x509.oid.ObjectIdentifier(dotted)
|
return x509.oid.ObjectIdentifier(dotted)
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_update(obj):
|
||||||
|
if CRYPTOGRAPHY_TIMEZONE:
|
||||||
|
return obj.next_update_utc
|
||||||
|
return obj.next_update
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_update(obj):
|
||||||
|
if CRYPTOGRAPHY_TIMEZONE:
|
||||||
|
return obj.last_update_utc
|
||||||
|
return obj.last_update
|
||||||
|
|
||||||
|
|
||||||
|
def get_revocation_date(obj):
|
||||||
|
if CRYPTOGRAPHY_TIMEZONE:
|
||||||
|
return obj.revocation_date_utc
|
||||||
|
return obj.revocation_date
|
||||||
|
|
||||||
|
|
||||||
|
def get_invalidity_date(obj):
|
||||||
|
if CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE:
|
||||||
|
return obj.invalidity_date_utc
|
||||||
|
return obj.invalidity_date
|
||||||
|
|
||||||
|
|
||||||
|
def set_next_update(builder, value):
|
||||||
|
return builder.next_update(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_last_update(builder, value):
|
||||||
|
return builder.last_update(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_revocation_date(builder, value):
|
||||||
|
return builder.revocation_date(value)
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
|
||||||
#
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# 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
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
@@ -22,22 +11,90 @@ __metaclass__ = type
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
|
||||||
|
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
|
||||||
from ._asn1 import serialize_asn1_string_as_der
|
from ._asn1 import serialize_asn1_string_as_der
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cryptography
|
import cryptography
|
||||||
from cryptography import x509
|
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 import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
_HAS_CRYPTOGRAPHY = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
_HAS_CRYPTOGRAPHY = False
|
||||||
# Error handled in the calling module.
|
# Error handled in the calling module.
|
||||||
pass
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
import idna
|
||||||
|
|
||||||
|
HAS_IDNA = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_IDNA = False
|
||||||
|
IDNA_IMP_ERROR = traceback.format_exc()
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
|
||||||
from .basic import (
|
from .basic import (
|
||||||
|
CRYPTOGRAPHY_HAS_DSA_SIGN,
|
||||||
|
CRYPTOGRAPHY_HAS_EC_SIGN,
|
||||||
CRYPTOGRAPHY_HAS_ED25519,
|
CRYPTOGRAPHY_HAS_ED25519,
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519_SIGN,
|
||||||
CRYPTOGRAPHY_HAS_ED448,
|
CRYPTOGRAPHY_HAS_ED448,
|
||||||
|
CRYPTOGRAPHY_HAS_ED448_SIGN,
|
||||||
|
CRYPTOGRAPHY_HAS_RSA_SIGN,
|
||||||
|
CRYPTOGRAPHY_HAS_X25519,
|
||||||
|
CRYPTOGRAPHY_HAS_X25519_FULL,
|
||||||
|
CRYPTOGRAPHY_HAS_X448,
|
||||||
OpenSSLObjectError,
|
OpenSSLObjectError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,64 +108,123 @@ from ._objects import (
|
|||||||
from ._obj2txt import obj2txt
|
from ._obj2txt import obj2txt
|
||||||
|
|
||||||
|
|
||||||
|
CRYPTOGRAPHY_TIMEZONE = False
|
||||||
|
if _HAS_CRYPTOGRAPHY:
|
||||||
|
CRYPTOGRAPHY_TIMEZONE = LooseVersion(cryptography.__version__) >= LooseVersion('42.0.0')
|
||||||
|
|
||||||
|
|
||||||
DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
|
DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
|
||||||
|
|
||||||
|
|
||||||
def cryptography_get_extensions_from_cert(cert):
|
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()
|
result = dict()
|
||||||
backend = cert._backend
|
try:
|
||||||
x509_obj = cert._x509
|
# Since cryptography will not give us the DER value for an extension
|
||||||
|
# (that is only stored for unrecognized extensions), we have to re-do
|
||||||
|
# the extension parsing ourselves.
|
||||||
|
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=to_native(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 its 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=to_native(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
|
return result
|
||||||
|
|
||||||
|
|
||||||
def cryptography_get_extensions_from_csr(csr):
|
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()
|
result = dict()
|
||||||
backend = csr._backend
|
try:
|
||||||
|
# Since cryptography will not give us the DER value for an extension
|
||||||
|
# (that is only stored for unrecognized extensions), we have to re-do
|
||||||
|
# the extension parsing ourselves.
|
||||||
|
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._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||||
extensions = backend._ffi.gc(
|
extensions = backend._ffi.gc(
|
||||||
extensions,
|
extensions,
|
||||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||||
ext,
|
ext,
|
||||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
|
||||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
# not allow to get the raw value of an extension, so we have to use this ugly hack:
|
||||||
if ext == backend._ffi.NULL:
|
exts = list(csr.extensions)
|
||||||
continue
|
|
||||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||||
backend.openssl_assert(data != backend._ffi.NULL)
|
if ext == backend._ffi.NULL:
|
||||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
continue
|
||||||
entry = dict(
|
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||||
critical=(crit == 1),
|
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||||
value=base64.b64encode(der),
|
backend.openssl_assert(data != backend._ffi.NULL)
|
||||||
)
|
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
entry = dict(
|
||||||
result[oid] = entry
|
critical=(crit == 1),
|
||||||
|
value=to_native(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 its 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=to_native(base64.b64encode(ext.value.public_bytes())),
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +268,70 @@ def _parse_hex(bytesstr):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
DN_COMPONENT_START_RE = re.compile(b'^ *([a-zA-z0-9.]+) *= *')
|
||||||
|
DN_HEX_LETTER = b'0123456789abcdef'
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
_int_to_byte = chr
|
||||||
|
else:
|
||||||
|
def _int_to_byte(value):
|
||||||
|
return bytes((value, ))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_dn_component(name, sep=b',', decode_remainder=True):
|
||||||
|
m = DN_COMPONENT_START_RE.match(name)
|
||||||
|
if not m:
|
||||||
|
raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name)))
|
||||||
|
oid = cryptography_name_to_oid(to_text(m.group(1)))
|
||||||
|
idx = len(m.group(0))
|
||||||
|
decoded_name = []
|
||||||
|
sep_str = sep + b'\\'
|
||||||
|
if decode_remainder:
|
||||||
|
length = len(name)
|
||||||
|
if length > idx and name[idx:idx + 1] == b'#':
|
||||||
|
# Decoding a hex string
|
||||||
|
idx += 1
|
||||||
|
while idx + 1 < length:
|
||||||
|
ch1 = name[idx:idx + 1]
|
||||||
|
ch2 = name[idx + 1:idx + 2]
|
||||||
|
idx1 = DN_HEX_LETTER.find(ch1.lower())
|
||||||
|
idx2 = DN_HEX_LETTER.find(ch2.lower())
|
||||||
|
if idx1 < 0 or idx2 < 0:
|
||||||
|
raise OpenSSLObjectError(u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2)))
|
||||||
|
idx += 2
|
||||||
|
decoded_name.append(_int_to_byte(idx1 * 16 + idx2))
|
||||||
|
else:
|
||||||
|
# Decoding a regular string
|
||||||
|
while idx < length:
|
||||||
|
i = idx
|
||||||
|
while i < length and name[i:i + 1] not in sep_str:
|
||||||
|
i += 1
|
||||||
|
if i > idx:
|
||||||
|
decoded_name.append(name[idx:i])
|
||||||
|
idx = i
|
||||||
|
while idx + 1 < length and name[idx:idx + 1] == b'\\':
|
||||||
|
ch = name[idx + 1:idx + 2]
|
||||||
|
idx1 = DN_HEX_LETTER.find(ch.lower())
|
||||||
|
if idx1 >= 0:
|
||||||
|
if idx + 2 >= length:
|
||||||
|
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" incomplete at end of string'.format(to_text(ch)))
|
||||||
|
ch2 = name[idx + 2:idx + 3]
|
||||||
|
idx2 = DN_HEX_LETTER.find(ch2.lower())
|
||||||
|
if idx2 < 0:
|
||||||
|
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" has invalid second letter'.format(to_text(ch + ch2)))
|
||||||
|
ch = _int_to_byte(idx1 * 16 + idx2)
|
||||||
|
idx += 1
|
||||||
|
idx += 2
|
||||||
|
decoded_name.append(ch)
|
||||||
|
if idx < length and name[idx:idx + 1] == sep:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
decoded_name.append(name[idx:])
|
||||||
|
idx = len(name)
|
||||||
|
return x509.NameAttribute(oid, to_text(b''.join(decoded_name))), name[idx:]
|
||||||
|
|
||||||
|
|
||||||
def _parse_dn(name):
|
def _parse_dn(name):
|
||||||
'''
|
'''
|
||||||
Parse a Distinguished Name.
|
Parse a Distinguished Name.
|
||||||
@@ -160,63 +340,129 @@ def _parse_dn(name):
|
|||||||
'''
|
'''
|
||||||
original_name = name
|
original_name = name
|
||||||
name = name.lstrip()
|
name = name.lstrip()
|
||||||
sep = ','
|
sep = b','
|
||||||
if name.startswith('/'):
|
if name.startswith(b'/'):
|
||||||
sep = '/'
|
sep = b'/'
|
||||||
name = name[1:]
|
name = name[1:]
|
||||||
sep_str = sep + '\\'
|
|
||||||
result = []
|
result = []
|
||||||
start_re = re.compile(r'^ *([a-zA-z0-9]+) *= *')
|
|
||||||
while name:
|
while name:
|
||||||
m = start_re.match(name)
|
try:
|
||||||
if not m:
|
attribute, name = _parse_dn_component(name, sep=sep)
|
||||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": cannot start part in "{1}"'.format(original_name, name))
|
except OpenSSLObjectError as e:
|
||||||
oid = cryptography_name_to_oid(m.group(1))
|
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": {1}'.format(to_text(original_name), e))
|
||||||
idx = len(m.group(0))
|
result.append(attribute)
|
||||||
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:]
|
|
||||||
if name:
|
if name:
|
||||||
if name[0] != sep or len(name) < 2:
|
if name[0:1] != sep or len(name) < 2:
|
||||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": unexpected end of string'.format(original_name))
|
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": unexpected end of string'.format(to_text(original_name)))
|
||||||
name = name[1:]
|
name = name[1:]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def cryptography_get_name(name):
|
def cryptography_parse_relative_distinguished_name(rdn):
|
||||||
|
names = []
|
||||||
|
for part in rdn:
|
||||||
|
try:
|
||||||
|
names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0])
|
||||||
|
except OpenSSLObjectError as e:
|
||||||
|
raise OpenSSLObjectError(u'Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
|
||||||
|
return cryptography.x509.RelativeDistinguishedName(names)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ascii(value):
|
||||||
|
'''Check whether the Unicode string `value` contains only ASCII characters.'''
|
||||||
|
try:
|
||||||
|
value.encode("ascii")
|
||||||
|
return True
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_idn(value, idn_rewrite):
|
||||||
|
if idn_rewrite == 'ignore' or not value:
|
||||||
|
return value
|
||||||
|
if idn_rewrite == 'idna' and _is_ascii(value):
|
||||||
|
return value
|
||||||
|
if idn_rewrite not in ('idna', 'unicode'):
|
||||||
|
raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite))
|
||||||
|
if not HAS_IDNA:
|
||||||
|
raise OpenSSLObjectError(
|
||||||
|
missing_required_lib('idna', reason='to transform {what} DNS name "{name}" to {dest}'.format(
|
||||||
|
name=value,
|
||||||
|
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
|
||||||
|
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
|
||||||
|
)))
|
||||||
|
# Since IDNA does not like '*' or empty labels (except one empty label at the end),
|
||||||
|
# we split and let IDNA only handle labels that are neither empty or '*'.
|
||||||
|
parts = value.split(u'.')
|
||||||
|
for index, part in enumerate(parts):
|
||||||
|
if part in (u'', u'*'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if idn_rewrite == 'idna':
|
||||||
|
parts[index] = idna.encode(part).decode('ascii')
|
||||||
|
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
|
||||||
|
parts[index] = idna.decode(part)
|
||||||
|
except idna.IDNAError as exc2008:
|
||||||
|
try:
|
||||||
|
if idn_rewrite == 'idna':
|
||||||
|
parts[index] = part.encode('idna').decode('ascii')
|
||||||
|
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
|
||||||
|
parts[index] = part.encode('ascii').decode('idna')
|
||||||
|
except Exception as exc2003:
|
||||||
|
raise OpenSSLObjectError(
|
||||||
|
u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.'
|
||||||
|
u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format(
|
||||||
|
part=part,
|
||||||
|
name=value,
|
||||||
|
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
|
||||||
|
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
|
||||||
|
exc2003=exc2003,
|
||||||
|
exc2008=exc2008,
|
||||||
|
))
|
||||||
|
return u'.'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_idn_email(value, idn_rewrite):
|
||||||
|
idx = value.find(u'@')
|
||||||
|
if idx < 0:
|
||||||
|
return value
|
||||||
|
return u'{0}@{1}'.format(value[:idx], _adjust_idn(value[idx + 1:], idn_rewrite))
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_idn_url(value, idn_rewrite):
|
||||||
|
url = urlparse(value)
|
||||||
|
host = _adjust_idn(url.hostname, idn_rewrite)
|
||||||
|
if url.username is not None and url.password is not None:
|
||||||
|
host = u'{0}:{1}@{2}'.format(url.username, url.password, host)
|
||||||
|
elif url.username is not None:
|
||||||
|
host = u'{0}@{1}'.format(url.username, host)
|
||||||
|
if url.port is not None:
|
||||||
|
host = u'{0}:{1}'.format(host, url.port)
|
||||||
|
return urlunparse(
|
||||||
|
ParseResult(scheme=url.scheme, netloc=host, path=url.path, params=url.params, query=url.query, fragment=url.fragment))
|
||||||
|
|
||||||
|
|
||||||
|
def cryptography_get_name(name, what='Subject Alternative Name'):
|
||||||
'''
|
'''
|
||||||
Given a name string, returns a cryptography x509.Name object.
|
Given a name string, returns a cryptography x509.GeneralName object.
|
||||||
Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
|
Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
if name.startswith('DNS:'):
|
if name.startswith('DNS:'):
|
||||||
return x509.DNSName(to_text(name[4:]))
|
return x509.DNSName(_adjust_idn(to_text(name[4:]), 'idna'))
|
||||||
if name.startswith('IP:'):
|
if name.startswith('IP:'):
|
||||||
address = to_text(name[3:])
|
address = to_text(name[3:])
|
||||||
if '/' in address:
|
if '/' in address:
|
||||||
return x509.IPAddress(ipaddress.ip_network(address))
|
return x509.IPAddress(ipaddress.ip_network(address))
|
||||||
return x509.IPAddress(ipaddress.ip_address(address))
|
return x509.IPAddress(ipaddress.ip_address(address))
|
||||||
if name.startswith('email:'):
|
if name.startswith('email:'):
|
||||||
return x509.RFC822Name(to_text(name[6:]))
|
return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), 'idna'))
|
||||||
if name.startswith('URI:'):
|
if name.startswith('URI:'):
|
||||||
return x509.UniformResourceIdentifier(to_text(name[4:]))
|
return x509.UniformResourceIdentifier(_adjust_idn_url(to_text(name[4:]), 'idna'))
|
||||||
if name.startswith('RID:'):
|
if name.startswith('RID:'):
|
||||||
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
|
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
|
||||||
if not m:
|
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)))
|
return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1)))
|
||||||
if name.startswith('otherName:'):
|
if name.startswith('otherName:'):
|
||||||
# otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with.
|
# otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with.
|
||||||
@@ -228,58 +474,65 @@ def cryptography_get_name(name):
|
|||||||
# defailts on the format expected.
|
# defailts on the format expected.
|
||||||
name = to_text(name[10:], errors='surrogate_or_strict')
|
name = to_text(name[10:], errors='surrogate_or_strict')
|
||||||
if ';' not in name:
|
if ';' not in name:
|
||||||
raise OpenSSLObjectError('Cannot parse 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 '
|
'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)
|
oid, value = name.split(';', 1)
|
||||||
b_value = serialize_asn1_string_as_der(value)
|
b_value = serialize_asn1_string_as_der(value)
|
||||||
return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
|
return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
|
||||||
if name.startswith('dirName:'):
|
if name.startswith('dirName:'):
|
||||||
return x509.DirectoryName(x509.Name(_parse_dn(to_text(name[8:]))))
|
return x509.DirectoryName(x509.Name(reversed(_parse_dn(to_bytes(name[8:])))))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise OpenSSLObjectError('Cannot parse 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:
|
if ':' not in name:
|
||||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name))
|
raise OpenSSLObjectError('Cannot parse {what} "{name}" (forgot "DNS:" prefix?)'.format(name=name, what=what))
|
||||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name))
|
raise OpenSSLObjectError('Cannot parse {what} "{name}" (potentially unsupported by cryptography backend)'.format(name=name, what=what))
|
||||||
|
|
||||||
|
|
||||||
def _dn_escape_value(value):
|
def _dn_escape_value(value):
|
||||||
'''
|
'''
|
||||||
Escape Distinguished Name's attribute value.
|
Escape Distinguished Name's attribute value.
|
||||||
'''
|
'''
|
||||||
value = value.replace('\\', '\\\\')
|
value = value.replace(u'\\', u'\\\\')
|
||||||
for ch in [',', '#', '+', '<', '>', ';', '"', '=', '/']:
|
for ch in [u',', u'+', u'<', u'>', u';', u'"']:
|
||||||
value = value.replace(ch, '\\%s' % ch)
|
value = value.replace(ch, u'\\%s' % ch)
|
||||||
if value.startswith(' '):
|
value = value.replace(u'\0', u'\\00')
|
||||||
value = r'\ ' + value[1:]
|
if value.startswith((u' ', u'#')):
|
||||||
|
value = u'\\%s' % value[0] + value[1:]
|
||||||
|
if value.endswith(u' '):
|
||||||
|
value = value[:-1] + u'\\ '
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def cryptography_decode_name(name):
|
def cryptography_decode_name(name, idn_rewrite='ignore'):
|
||||||
'''
|
'''
|
||||||
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.
|
Raises an OpenSSLObjectError if the name is not supported.
|
||||||
'''
|
'''
|
||||||
|
if idn_rewrite not in ('ignore', 'idna', 'unicode'):
|
||||||
|
raise AssertionError('idn_rewrite must be one of "ignore", "idna", or "unicode"')
|
||||||
if isinstance(name, x509.DNSName):
|
if isinstance(name, x509.DNSName):
|
||||||
return 'DNS:{0}'.format(name.value)
|
return u'DNS:{0}'.format(_adjust_idn(name.value, idn_rewrite))
|
||||||
if isinstance(name, x509.IPAddress):
|
if isinstance(name, x509.IPAddress):
|
||||||
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
||||||
return 'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
||||||
return 'IP:{0}'.format(name.value.compressed)
|
return u'IP:{0}'.format(name.value.compressed)
|
||||||
if isinstance(name, x509.RFC822Name):
|
if isinstance(name, x509.RFC822Name):
|
||||||
return 'email:{0}'.format(name.value)
|
return u'email:{0}'.format(_adjust_idn_email(name.value, idn_rewrite))
|
||||||
if isinstance(name, x509.UniformResourceIdentifier):
|
if isinstance(name, x509.UniformResourceIdentifier):
|
||||||
return 'URI:{0}'.format(name.value)
|
return u'URI:{0}'.format(_adjust_idn_url(name.value, idn_rewrite))
|
||||||
if isinstance(name, x509.DirectoryName):
|
if isinstance(name, x509.DirectoryName):
|
||||||
return 'dirName:' + ''.join([
|
# According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
|
||||||
'/{0}={1}'.format(cryptography_oid_to_name(attribute.oid, short=True), _dn_escape_value(attribute.value))
|
# list needs to be reversed, and joined by commas
|
||||||
for attribute in name.value
|
return u'dirName:' + ','.join([
|
||||||
|
u'{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
|
||||||
|
for attribute in reversed(list(name.value))
|
||||||
])
|
])
|
||||||
if isinstance(name, x509.RegisteredID):
|
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):
|
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))
|
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
|
||||||
|
|
||||||
|
|
||||||
@@ -369,32 +622,75 @@ def cryptography_key_needs_digest_for_signing(key):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_public_keys(key1, key2, clazz):
|
||||||
|
a = isinstance(key1, clazz)
|
||||||
|
b = isinstance(key2, clazz)
|
||||||
|
if not (a or b):
|
||||||
|
return None
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||||
|
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||||
|
return a == b
|
||||||
|
|
||||||
|
|
||||||
def cryptography_compare_public_keys(key1, key2):
|
def cryptography_compare_public_keys(key1, key2):
|
||||||
'''Tests whether two public keys are the same.
|
'''Tests whether two public keys are the same.
|
||||||
|
|
||||||
Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers().
|
Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers().
|
||||||
'''
|
'''
|
||||||
if CRYPTOGRAPHY_HAS_ED25519:
|
if CRYPTOGRAPHY_HAS_ED25519:
|
||||||
a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
|
res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
|
||||||
b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
|
if res is not None:
|
||||||
if a or b:
|
return res
|
||||||
if not a or not b:
|
|
||||||
return False
|
|
||||||
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
||||||
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
||||||
return a == b
|
|
||||||
if CRYPTOGRAPHY_HAS_ED448:
|
if CRYPTOGRAPHY_HAS_ED448:
|
||||||
a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
|
res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
|
||||||
b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
|
if res is not None:
|
||||||
if a or b:
|
return res
|
||||||
if not a or not b:
|
|
||||||
return False
|
|
||||||
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
||||||
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
||||||
return a == b
|
|
||||||
return key1.public_numbers() == key2.public_numbers()
|
return key1.public_numbers() == key2.public_numbers()
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_private_keys(key1, key2, clazz, has_no_private_bytes=False):
|
||||||
|
a = isinstance(key1, clazz)
|
||||||
|
b = isinstance(key2, clazz)
|
||||||
|
if not (a or b):
|
||||||
|
return None
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
if has_no_private_bytes:
|
||||||
|
# We do not have the private_bytes() function - compare associated public keys
|
||||||
|
return cryptography_compare_public_keys(a.public_key(), b.public_key())
|
||||||
|
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
|
||||||
|
a = key1.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm)
|
||||||
|
b = key2.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm)
|
||||||
|
return a == b
|
||||||
|
|
||||||
|
|
||||||
|
def cryptography_compare_private_keys(key1, key2):
|
||||||
|
'''Tests whether two private keys are the same.
|
||||||
|
|
||||||
|
Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers().
|
||||||
|
'''
|
||||||
|
if CRYPTOGRAPHY_HAS_ED25519:
|
||||||
|
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey)
|
||||||
|
if res is not None:
|
||||||
|
return res
|
||||||
|
if CRYPTOGRAPHY_HAS_X25519:
|
||||||
|
res = _compare_private_keys(
|
||||||
|
key1, key2, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey, has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL)
|
||||||
|
if res is not None:
|
||||||
|
return res
|
||||||
|
if CRYPTOGRAPHY_HAS_ED448:
|
||||||
|
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey)
|
||||||
|
if res is not None:
|
||||||
|
return res
|
||||||
|
if CRYPTOGRAPHY_HAS_X448:
|
||||||
|
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey)
|
||||||
|
if res is not None:
|
||||||
|
return res
|
||||||
|
return key1.private_numbers() == key2.private_numbers()
|
||||||
|
|
||||||
|
|
||||||
def cryptography_serial_number_of_cert(cert):
|
def cryptography_serial_number_of_cert(cert):
|
||||||
'''Returns cert.serial_number.
|
'''Returns cert.serial_number.
|
||||||
|
|
||||||
@@ -405,3 +701,136 @@ def cryptography_serial_number_of_cert(cert):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
# The property was called "serial" before cryptography 1.4
|
# The property was called "serial" before cryptography 1.4
|
||||||
return cert.serial
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_not_valid_after(obj):
|
||||||
|
if CRYPTOGRAPHY_TIMEZONE:
|
||||||
|
return obj.not_valid_after_utc
|
||||||
|
return obj.not_valid_after
|
||||||
|
|
||||||
|
|
||||||
|
def get_not_valid_before(obj):
|
||||||
|
if CRYPTOGRAPHY_TIMEZONE:
|
||||||
|
return obj.not_valid_before_utc
|
||||||
|
return obj.not_valid_before
|
||||||
|
|
||||||
|
|
||||||
|
def set_not_valid_after(builder, value):
|
||||||
|
return builder.not_valid_after(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_not_valid_before(builder, value):
|
||||||
|
return builder.not_valid_before(value)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
|
||||||
#
|
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Ansible is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
PEM_START = '-----BEGIN '
|
|
||||||
PEM_END = '-----'
|
|
||||||
PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY')
|
|
||||||
PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY'
|
|
||||||
|
|
||||||
|
|
||||||
def identify_pem_format(content):
|
|
||||||
'''Given the contents of a binary file, tests whether this could be a PEM file.'''
|
|
||||||
try:
|
|
||||||
lines = content.decode('utf-8').splitlines(False)
|
|
||||||
if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END):
|
|
||||||
return True
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def identify_private_key_format(content):
|
|
||||||
'''Given the contents of a private key file, identifies its format.'''
|
|
||||||
# See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85
|
|
||||||
# (PEM_read_bio_PrivateKey)
|
|
||||||
# and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47
|
|
||||||
# (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF)
|
|
||||||
try:
|
|
||||||
lines = content.decode('utf-8').splitlines(False)
|
|
||||||
if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END):
|
|
||||||
name = lines[0][len(PEM_START):-len(PEM_END)]
|
|
||||||
if name in PKCS8_PRIVATEKEY_NAMES:
|
|
||||||
return 'pkcs8'
|
|
||||||
if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX):
|
|
||||||
return 'pkcs1'
|
|
||||||
return 'unknown-pem'
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
pass
|
|
||||||
return 'raw'
|
|
||||||
@@ -1,19 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
|
||||||
#
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# Ansible is free software: you can redistribute it and/or modify
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# 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
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
@@ -50,12 +39,21 @@ def quick_is_not_prime(n):
|
|||||||
'''Does some quick checks to see if we can poke a hole into the primality of n.
|
'''Does some quick checks to see if we can poke a hole into the primality of n.
|
||||||
|
|
||||||
A result of `False` does **not** mean that the number is prime; it just means
|
A result of `False` does **not** mean that the number is prime; it just means
|
||||||
that we couldn't detect quickly whether it is not prime.
|
that we could not detect quickly whether it is not prime.
|
||||||
'''
|
'''
|
||||||
if n <= 2:
|
if n <= 2:
|
||||||
return True
|
return n < 2
|
||||||
# The constant in the next line is the product of all primes < 200
|
# The constant in the next line is the product of all primes < 200
|
||||||
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
|
return True
|
||||||
# TODO: maybe do some iterations of Miller-Rabin to increase confidence
|
# TODO: maybe do some iterations of Miller-Rabin to increase confidence
|
||||||
# (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
|
# (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
|
||||||
@@ -65,17 +63,111 @@ def quick_is_not_prime(n):
|
|||||||
python_version = (sys.version_info[0], sys.version_info[1])
|
python_version = (sys.version_info[0], sys.version_info[1])
|
||||||
if python_version >= (2, 7) or python_version >= (3, 1):
|
if python_version >= (2, 7) or python_version >= (3, 1):
|
||||||
# Ansible still supports Python 2.6 on remote nodes
|
# Ansible still supports Python 2.6 on remote nodes
|
||||||
|
|
||||||
|
def count_bytes(no):
|
||||||
|
"""
|
||||||
|
Given an integer, compute the number of bytes necessary to store its absolute value.
|
||||||
|
"""
|
||||||
|
no = abs(no)
|
||||||
|
if no == 0:
|
||||||
|
return 0
|
||||||
|
return (no.bit_length() + 7) // 8
|
||||||
|
|
||||||
def count_bits(no):
|
def count_bits(no):
|
||||||
|
"""
|
||||||
|
Given an integer, compute the number of bits necessary to store its absolute value.
|
||||||
|
"""
|
||||||
no = abs(no)
|
no = abs(no)
|
||||||
if no == 0:
|
if no == 0:
|
||||||
return 0
|
return 0
|
||||||
return no.bit_length()
|
return no.bit_length()
|
||||||
else:
|
else:
|
||||||
# Slow, but works
|
# Slow, but works
|
||||||
|
def count_bytes(no):
|
||||||
|
"""
|
||||||
|
Given an integer, compute the number of bytes necessary to store its absolute value.
|
||||||
|
"""
|
||||||
|
no = abs(no)
|
||||||
|
count = 0
|
||||||
|
while no > 0:
|
||||||
|
no >>= 8
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
def count_bits(no):
|
def count_bits(no):
|
||||||
|
"""
|
||||||
|
Given an integer, compute the number of bits necessary to store its absolute value.
|
||||||
|
"""
|
||||||
no = abs(no)
|
no = abs(no)
|
||||||
count = 0
|
count = 0
|
||||||
while no > 0:
|
while no > 0:
|
||||||
no >>= 1
|
no >>= 1
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
# Python 3 (and newer)
|
||||||
|
def _convert_int_to_bytes(count, no):
|
||||||
|
return no.to_bytes(count, byteorder='big')
|
||||||
|
|
||||||
|
def _convert_bytes_to_int(data):
|
||||||
|
return int.from_bytes(data, byteorder='big', signed=False)
|
||||||
|
|
||||||
|
def _to_hex(no):
|
||||||
|
return hex(no)[2:]
|
||||||
|
else:
|
||||||
|
# Python 2
|
||||||
|
def _convert_int_to_bytes(count, n):
|
||||||
|
if n == 0 and count == 0:
|
||||||
|
return ''
|
||||||
|
h = '%x' % n
|
||||||
|
if len(h) > 2 * count:
|
||||||
|
raise Exception('Number {1} needs more than {0} bytes!'.format(count, n))
|
||||||
|
return ('0' * (2 * count - len(h)) + h).decode('hex')
|
||||||
|
|
||||||
|
def _convert_bytes_to_int(data):
|
||||||
|
v = 0
|
||||||
|
for x in data:
|
||||||
|
v = (v << 8) | ord(x)
|
||||||
|
return v
|
||||||
|
|
||||||
|
def _to_hex(no):
|
||||||
|
return '%x' % no
|
||||||
|
|
||||||
|
|
||||||
|
def convert_int_to_bytes(no, count=None):
|
||||||
|
"""
|
||||||
|
Convert the absolute value of an integer to a byte string in network byte order.
|
||||||
|
|
||||||
|
If ``count`` is provided, it must be sufficiently large so that the integer's
|
||||||
|
absolute value can be represented with these number of bytes. The resulting byte
|
||||||
|
string will have length exactly ``count``.
|
||||||
|
|
||||||
|
The value zero will be converted to an empty byte string if ``count`` is provided.
|
||||||
|
"""
|
||||||
|
no = abs(no)
|
||||||
|
if count is None:
|
||||||
|
count = count_bytes(no)
|
||||||
|
return _convert_int_to_bytes(count, no)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_int_to_hex(no, digits=None):
|
||||||
|
"""
|
||||||
|
Convert the absolute value of an integer to a string of hexadecimal digits.
|
||||||
|
|
||||||
|
If ``digits`` is provided, the string will be padded on the left with ``0``s so
|
||||||
|
that the returned value has length ``digits``. If ``digits`` is not sufficient,
|
||||||
|
the string will be longer.
|
||||||
|
"""
|
||||||
|
no = abs(no)
|
||||||
|
value = _to_hex(no)
|
||||||
|
if digits is not None and len(value) < digits:
|
||||||
|
value = '0' * (digits - len(value)) + value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def convert_bytes_to_int(data):
|
||||||
|
"""
|
||||||
|
Convert a byte string to an unsigned integer in network byte order.
|
||||||
|
"""
|
||||||
|
return _convert_bytes_to_int(data)
|
||||||
|
|||||||
356
plugins/module_utils/crypto/module_backends/certificate.py
Normal file
356
plugins/module_utils/crypto/module_backends/certificate.py
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
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.argspec import ArgumentSpec
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
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,
|
||||||
|
get_not_valid_after,
|
||||||
|
get_not_valid_before,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
|
||||||
|
get_certificate_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||||
|
|
||||||
|
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.ignore_timestamps = module.params['ignore_timestamps']
|
||||||
|
self.privatekey_path = module.params['privatekey_path']
|
||||||
|
self.privatekey_content = module.params['privatekey_content']
|
||||||
|
if self.privatekey_content is not None:
|
||||||
|
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 == '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 == '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."""
|
||||||
|
# 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 did not ignore it ('create_if_not_provided'), compare SKIs
|
||||||
|
if ext.value.digest != csr_ext.value.digest:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def needs_regeneration(self, not_before=None, not_after=None):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
# Check not before
|
||||||
|
if not_before is not None and not self.ignore_timestamps:
|
||||||
|
if get_not_valid_before(self.existing_certificate) != not_before:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check not after
|
||||||
|
if not_after is not None and not self.ignore_timestamps:
|
||||||
|
if get_not_valid_after(self.existing_certificate) != not_after:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def dump(self, include_certificate):
|
||||||
|
"""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."""
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
# If cryptography is available we'll use it
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
|
||||||
|
# Fail if no backend has been found
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Cannot detect the required Python library "
|
||||||
|
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||||
|
|
||||||
|
if backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
if provider.needs_version_two_certs(module):
|
||||||
|
module.fail_json(msg='The cryptography backend does not support 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'),
|
||||||
|
ignore_timestamps=dict(type='bool', default=True),
|
||||||
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||||
|
|
||||||
|
# General properties of a certificate
|
||||||
|
privatekey_path=dict(type='path'),
|
||||||
|
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'],
|
||||||
|
],
|
||||||
|
)
|
||||||
120
plugins/module_utils/crypto/module_backends/certificate_acme.py
Normal file
120
plugins/module_utils/crypto/module_backends/certificate_acme.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
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,221 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
cryptography_serial_number_of_cert,
|
||||||
|
get_not_valid_after,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||||
|
CertificateError,
|
||||||
|
CertificateBackend,
|
||||||
|
CertificateProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import (
|
||||||
|
get_now_datetime,
|
||||||
|
get_relative_time_option,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
except ImportError:
|
||||||
|
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,
|
||||||
|
with_timezone=CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 == '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 = get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
|
||||||
|
expiry = gmt_now + datetime.timedelta(days=365)
|
||||||
|
|
||||||
|
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
|
||||||
|
body['certExpiryDate'] = expiry_iso3339
|
||||||
|
body['org'] = self.csr_org
|
||||||
|
body['tracking'] = {
|
||||||
|
'requesterName': self.module.params['entrust_requester_name'],
|
||||||
|
'requesterEmail': self.module.params['entrust_requester_email'],
|
||||||
|
'requesterPhone': self.module.params['entrust_requester_phone'],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.ecs_client.NewCertRequest(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 == 'cryptography':
|
||||||
|
serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate))
|
||||||
|
expiry = get_not_valid_after(self.existing_certificate)
|
||||||
|
|
||||||
|
# get some information about the expiry of this certificate
|
||||||
|
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
|
||||||
|
cert_details['expiresAfter'] = expiry_iso3339
|
||||||
|
|
||||||
|
# If a trackingId is not already defined (from the result of a generate)
|
||||||
|
# use the serial number to identify the tracking Id
|
||||||
|
if self.trackingId is None and serial_number is not None:
|
||||||
|
cert_results = self.ecs_client.GetCertificates(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']]
|
||||||
|
)
|
||||||
417
plugins/module_utils/crypto/module_backends/certificate_info.py
Normal file
417
plugins/module_utils/crypto/module_backends/certificate_info.py
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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
|
||||||
|
|
||||||
|
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_TIMEZONE,
|
||||||
|
cryptography_decode_name,
|
||||||
|
cryptography_get_extensions_from_cert,
|
||||||
|
cryptography_oid_to_name,
|
||||||
|
cryptography_serial_number_of_cert,
|
||||||
|
get_not_valid_after,
|
||||||
|
get_not_valid_before,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||||
|
get_publickey_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import (
|
||||||
|
get_now_datetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||||
|
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = None
|
||||||
|
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
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_issuer_uri(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_info(self, prefer_one_fingerprint=False, der_support_enabled=False):
|
||||||
|
result = dict()
|
||||||
|
self.cert = load_certificate(None, content=self.content, backend=self.backend, der_support_enabled=der_support_enabled)
|
||||||
|
|
||||||
|
result['signature_algorithm'] = self._get_signature_algorithm()
|
||||||
|
subject = self._get_subject_ordered()
|
||||||
|
issuer = self._get_issuer_ordered()
|
||||||
|
result['subject'] = 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 < get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
|
||||||
|
|
||||||
|
result['public_key'] = to_native(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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
result['issuer_uri'] = self._get_issuer_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)
|
||||||
|
self.name_encoding = module.params.get('name_encoding', 'ignore')
|
||||||
|
|
||||||
|
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, idn_rewrite=self.name_encoding) for san in san_ext.value]
|
||||||
|
return result, san_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def get_not_before(self):
|
||||||
|
return get_not_valid_before(self.cert)
|
||||||
|
|
||||||
|
def get_not_after(self):
|
||||||
|
return get_not_valid_after(self.cert)
|
||||||
|
|
||||||
|
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, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer]
|
||||||
|
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def _get_serial_number(self):
|
||||||
|
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
|
||||||
|
|
||||||
|
def _get_issuer_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.CA_ISSUERS:
|
||||||
|
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
|
||||||
|
return desc.access_location.value
|
||||||
|
except x509.ExtensionNotFound as dummy:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_certificate_info(module, backend, content, prefer_one_fingerprint=False):
|
||||||
|
if backend == 'cryptography':
|
||||||
|
info = CertificateInfoRetrievalCryptography(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)
|
||||||
|
|
||||||
|
# Try cryptography
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Cannot detect any of the required Python libraries "
|
||||||
|
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||||
|
|
||||||
|
if backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
return backend, CertificateInfoRetrievalCryptography(module, content)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
294
plugins/module_utils/crypto/module_backends/certificate_ownca.py
Normal file
294
plugins/module_utils/crypto/module_backends/certificate_ownca.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from random import randrange
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLBadPassphraseError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
load_privatekey,
|
||||||
|
load_certificate,
|
||||||
|
select_message_digest,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
cryptography_compare_public_keys,
|
||||||
|
cryptography_key_needs_digest_for_signing,
|
||||||
|
cryptography_serial_number_of_cert,
|
||||||
|
cryptography_verify_certificate_signature,
|
||||||
|
get_not_valid_after,
|
||||||
|
get_not_valid_before,
|
||||||
|
set_not_valid_after,
|
||||||
|
set_not_valid_before,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||||
|
CRYPTOGRAPHY_VERSION,
|
||||||
|
CertificateError,
|
||||||
|
CertificateBackend,
|
||||||
|
CertificateProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import (
|
||||||
|
get_relative_time_option,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
with_timezone=CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
)
|
||||||
|
self.notAfter = get_relative_time_option(
|
||||||
|
module.params['ownca_not_after'],
|
||||||
|
'ownca_not_after',
|
||||||
|
backend=self.backend,
|
||||||
|
with_timezone=CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
)
|
||||||
|
self.digest = select_message_digest(module.params['ownca_digest'])
|
||||||
|
self.version = module.params['ownca_version']
|
||||||
|
self.serial_number = x509.random_serial_number()
|
||||||
|
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 = set_not_valid_before(cert_builder, self.notBefore)
|
||||||
|
cert_builder = set_not_valid_after(cert_builder, self.notAfter)
|
||||||
|
cert_builder = cert_builder.public_key(self.csr.public_key())
|
||||||
|
has_ski = False
|
||||||
|
for extension in self.csr.extensions:
|
||||||
|
if isinstance(extension.value, x509.SubjectKeyIdentifier):
|
||||||
|
if self.create_subject_key_identifier == 'always_create':
|
||||||
|
continue
|
||||||
|
has_ski = True
|
||||||
|
if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier):
|
||||||
|
continue
|
||||||
|
cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical)
|
||||||
|
if not has_ski and self.create_subject_key_identifier != 'never_create':
|
||||||
|
cert_builder = cert_builder.add_extension(
|
||||||
|
x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()),
|
||||||
|
critical=False
|
||||||
|
)
|
||||||
|
if self.create_authority_key_identifier:
|
||||||
|
try:
|
||||||
|
ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||||
|
cert_builder = cert_builder.add_extension(
|
||||||
|
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value)
|
||||||
|
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(not_before=self.notBefore, not_after=self.notAfter):
|
||||||
|
return True
|
||||||
|
|
||||||
|
self._ensure_existing_certificate_loaded()
|
||||||
|
|
||||||
|
# Check whether certificate is signed by CA certificate
|
||||||
|
if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check subject
|
||||||
|
if self.ca_cert.subject != self.existing_certificate.issuer:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check AuthorityKeyIdentifier
|
||||||
|
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': get_not_valid_before(self.cert).strftime("%Y%m%d%H%M%SZ"),
|
||||||
|
'notAfter': get_not_valid_after(self.cert).strftime("%Y%m%d%H%M%SZ"),
|
||||||
|
'serial_number': cryptography_serial_number_of_cert(self.cert),
|
||||||
|
})
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
|
||||||
|
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,216 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from random import randrange
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
select_message_digest,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
cryptography_key_needs_digest_for_signing,
|
||||||
|
cryptography_serial_number_of_cert,
|
||||||
|
cryptography_verify_certificate_signature,
|
||||||
|
get_not_valid_after,
|
||||||
|
get_not_valid_before,
|
||||||
|
set_not_valid_after,
|
||||||
|
set_not_valid_before,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||||
|
CertificateError,
|
||||||
|
CertificateBackend,
|
||||||
|
CertificateProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.time import (
|
||||||
|
get_relative_time_option,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
with_timezone=CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
)
|
||||||
|
self.notAfter = get_relative_time_option(
|
||||||
|
module.params['selfsigned_not_after'],
|
||||||
|
'selfsigned_not_after',
|
||||||
|
backend=self.backend,
|
||||||
|
with_timezone=CRYPTOGRAPHY_TIMEZONE,
|
||||||
|
)
|
||||||
|
self.digest = select_message_digest(module.params['selfsigned_digest'])
|
||||||
|
self.version = module.params['selfsigned_version']
|
||||||
|
self.serial_number = x509.random_serial_number()
|
||||||
|
|
||||||
|
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 = set_not_valid_before(cert_builder, self.notBefore)
|
||||||
|
cert_builder = set_not_valid_after(cert_builder, self.notAfter)
|
||||||
|
cert_builder = cert_builder.public_key(self.privatekey.public_key())
|
||||||
|
has_ski = False
|
||||||
|
for extension in self.csr.extensions:
|
||||||
|
if isinstance(extension.value, x509.SubjectKeyIdentifier):
|
||||||
|
if self.create_subject_key_identifier == 'always_create':
|
||||||
|
continue
|
||||||
|
has_ski = True
|
||||||
|
cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical)
|
||||||
|
if not has_ski and self.create_subject_key_identifier != 'never_create':
|
||||||
|
cert_builder = cert_builder.add_extension(
|
||||||
|
x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()),
|
||||||
|
critical=False
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise CertificateError(str(e))
|
||||||
|
|
||||||
|
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(not_before=self.notBefore, not_after=self.notAfter):
|
||||||
|
return True
|
||||||
|
|
||||||
|
self._ensure_existing_certificate_loaded()
|
||||||
|
|
||||||
|
# Check whether certificate is signed by private key
|
||||||
|
if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def dump(self, include_certificate):
|
||||||
|
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate)
|
||||||
|
|
||||||
|
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': get_not_valid_before(self.cert).strftime("%Y%m%d%H%M%SZ"),
|
||||||
|
'notAfter': get_not_valid_after(self.cert).strftime("%Y%m%d%H%M%SZ"),
|
||||||
|
'serial_number': cryptography_serial_number_of_cert(self.cert),
|
||||||
|
})
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
|
||||||
|
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']
|
||||||
|
),
|
||||||
|
))
|
||||||
28
plugins/module_utils/crypto/module_backends/common.py
Normal file
28
plugins/module_utils/crypto/module_backends/common.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec as _ArgumentSpec
|
||||||
|
|
||||||
|
|
||||||
|
class ArgumentSpec(_ArgumentSpec):
|
||||||
|
def create_ansible_module_helper(self, clazz, args, **kwargs):
|
||||||
|
result = super(ArgumentSpec, self).create_ansible_module_helper(clazz, args, **kwargs)
|
||||||
|
result.deprecate(
|
||||||
|
"The crypto.module_backends.common module utils is deprecated and will be removed from community.crypto 3.0.0."
|
||||||
|
" Use the argspec module utils from community.crypto instead.",
|
||||||
|
version='3.0.0',
|
||||||
|
collection_name='community.crypto',
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('AnsibleModule', 'ArgumentSpec')
|
||||||
102
plugins/module_utils/crypto/module_backends/crl_info.py
Normal file
102
plugins/module_utils/crypto/module_backends/crl_info.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
self.name_encoding = module.params.get('name_encoding', 'ignore')
|
||||||
|
|
||||||
|
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, idn_rewrite=self.name_encoding))
|
||||||
|
|
||||||
|
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()
|
||||||
675
plugins/module_utils/crypto/module_backends/csr.py
Normal file
675
plugins/module_utils/crypto/module_backends/csr.py
Normal file
@@ -0,0 +1,675 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
OpenSSLBadPassphraseError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
load_privatekey,
|
||||||
|
load_certificate_request,
|
||||||
|
parse_name_field,
|
||||||
|
parse_ordered_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.module_backends.csr_info import (
|
||||||
|
get_csr_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||||
|
|
||||||
|
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.ordered_subject = False
|
||||||
|
self.subject = [
|
||||||
|
('C', module.params['country_name']),
|
||||||
|
('ST', module.params['state_or_province_name']),
|
||||||
|
('L', module.params['locality_name']),
|
||||||
|
('O', module.params['organization_name']),
|
||||||
|
('OU', module.params['organizational_unit_name']),
|
||||||
|
('CN', module.params['common_name']),
|
||||||
|
('emailAddress', module.params['email_address']),
|
||||||
|
]
|
||||||
|
self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if module.params['subject']:
|
||||||
|
self.subject = self.subject + parse_name_field(module.params['subject'], 'subject')
|
||||||
|
if module.params['subject_ordered']:
|
||||||
|
if self.subject:
|
||||||
|
raise CertificateSigningRequestError('subject_ordered cannot be combined with any other subject field')
|
||||||
|
self.subject = parse_ordered_name_field(module.params['subject_ordered'], 'subject_ordered')
|
||||||
|
self.ordered_subject = True
|
||||||
|
except ValueError as exc:
|
||||||
|
raise CertificateSigningRequestError(to_native(exc))
|
||||||
|
|
||||||
|
self.using_common_name_for_san = False
|
||||||
|
if not self.subjectAltName and module.params['use_common_name_for_san']:
|
||||||
|
for sub in self.subject:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
if self.ordered_subject:
|
||||||
|
return subject == current_subject
|
||||||
|
else:
|
||||||
|
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 backend == 'auto':
|
||||||
|
# Detection what is possible
|
||||||
|
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||||
|
|
||||||
|
# Try cryptography
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Cannot detect any of the required Python libraries "
|
||||||
|
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||||
|
|
||||||
|
if backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
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, choices=[1]),
|
||||||
|
subject=dict(type='dict'),
|
||||||
|
subject_ordered=dict(type='list', elements='dict'),
|
||||||
|
country_name=dict(type='str', aliases=['C', 'countryName']),
|
||||||
|
state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']),
|
||||||
|
locality_name=dict(type='str', aliases=['L', 'localityName']),
|
||||||
|
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']),
|
||||||
|
),
|
||||||
|
required_together=[
|
||||||
|
['authority_cert_issuer', 'authority_cert_serial_number'],
|
||||||
|
],
|
||||||
|
mutually_exclusive=[
|
||||||
|
['privatekey_path', 'privatekey_content'],
|
||||||
|
['subject', 'subject_ordered'],
|
||||||
|
],
|
||||||
|
required_one_of=[
|
||||||
|
['privatekey_path', 'privatekey_content'],
|
||||||
|
],
|
||||||
|
)
|
||||||
334
plugins/module_utils/crypto/module_backends/csr_info.py
Normal file
334
plugins/module_utils/crypto/module_backends/csr_info.py
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import 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
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
load_certificate_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
cryptography_decode_name,
|
||||||
|
cryptography_get_extensions_from_csr,
|
||||||
|
cryptography_oid_to_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||||
|
get_publickey_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||||
|
|
||||||
|
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'] = to_native(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'],
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.name_encoding = module.params.get('name_encoding', 'ignore')
|
||||||
|
|
||||||
|
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, idn_rewrite=self.name_encoding) for san in san_ext.value]
|
||||||
|
return result, san_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_name_constraints(self):
|
||||||
|
try:
|
||||||
|
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
|
||||||
|
permitted = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.permitted_subtrees or []]
|
||||||
|
excluded = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.excluded_subtrees or []]
|
||||||
|
return permitted, excluded, nc_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, None, False
|
||||||
|
|
||||||
|
def _get_public_key_pem(self):
|
||||||
|
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, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer]
|
||||||
|
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def _get_all_extensions(self):
|
||||||
|
return cryptography_get_extensions_from_csr(self.csr)
|
||||||
|
|
||||||
|
def _is_signature_valid(self):
|
||||||
|
return self.csr.is_signature_valid
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Try cryptography
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Cannot detect the required Python library "
|
||||||
|
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||||
|
|
||||||
|
if backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
return backend, CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user