test_x509.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. import datetime
  2. import hashlib
  3. import logging
  4. import os
  5. import pprint
  6. import textwrap
  7. import pytest
  8. import salt.utils.files
  9. from tests.support.case import ModuleCase
  10. from tests.support.helpers import slowTest, with_tempfile
  11. from tests.support.mixins import SaltReturnAssertsMixin
  12. from tests.support.runtests import RUNTIME_VARS
  13. from tests.support.unit import skipIf
  14. try:
  15. import M2Crypto # pylint: disable=W0611
  16. HAS_M2CRYPTO = True
  17. except ImportError:
  18. HAS_M2CRYPTO = False
  19. log = logging.getLogger(__name__)
  20. @pytest.mark.usefixtures("salt_sub_minion")
  21. @skipIf(not HAS_M2CRYPTO, "Skip when no M2Crypto found")
  22. class x509Test(ModuleCase, SaltReturnAssertsMixin):
  23. @classmethod
  24. def setUpClass(cls):
  25. cert_path = os.path.join(RUNTIME_VARS.BASE_FILES, "x509_test.crt")
  26. with salt.utils.files.fopen(cert_path) as fp:
  27. cls.x509_cert_text = fp.read()
  28. def setUp(self):
  29. with salt.utils.files.fopen(
  30. os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "signing_policies.sls"), "w"
  31. ) as fp:
  32. fp.write(
  33. textwrap.dedent(
  34. """\
  35. x509_signing_policies:
  36. ca_policy:
  37. - minions: '*'
  38. - signing_private_key: {0}/pki/ca.key
  39. - signing_cert: {0}/pki/ca.crt
  40. - O: Test Company
  41. - basicConstraints: "CA:false"
  42. - keyUsage: "critical digitalSignature, keyEncipherment"
  43. - extendedKeyUsage: "critical serverAuth, clientAuth"
  44. - subjectKeyIdentifier: hash
  45. - authorityKeyIdentifier: keyid
  46. - days_valid: 730
  47. - copypath: {0}/pki
  48. compound_match:
  49. - minions: 'G@x509_test_grain:correct_value'
  50. - signing_private_key: {0}/pki/ca.key
  51. - signing_cert: {0}/pki/ca.crt
  52. - O: Test Company
  53. - basicConstraints: "CA:false"
  54. - keyUsage: "critical digitalSignature, keyEncipherment"
  55. - extendedKeyUsage: "critical serverAuth, clientAuth"
  56. - subjectKeyIdentifier: hash
  57. - authorityKeyIdentifier: keyid
  58. - days_valid: 730
  59. - copypath: {0}/pki
  60. """.format(
  61. RUNTIME_VARS.TMP
  62. )
  63. )
  64. )
  65. with salt.utils.files.fopen(
  66. os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "top.sls"), "w"
  67. ) as fp:
  68. fp.write(
  69. textwrap.dedent(
  70. """\
  71. base:
  72. '*':
  73. - signing_policies
  74. """
  75. )
  76. )
  77. self.run_function("saltutil.refresh_pillar")
  78. self.run_function(
  79. "grains.set", ["x509_test_grain", "correct_value"], minion_tgt="sub_minion"
  80. )
  81. self.run_function(
  82. "grains.set", ["x509_test_grain", "not_correct_value"], minion_tgt="minion"
  83. )
  84. def tearDown(self):
  85. os.remove(os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "signing_policies.sls"))
  86. os.remove(os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "top.sls"))
  87. certs_path = os.path.join(RUNTIME_VARS.TMP, "pki")
  88. if os.path.exists(certs_path):
  89. salt.utils.files.rm_rf(certs_path)
  90. self.run_function("saltutil.refresh_pillar")
  91. self.run_function("grains.delkey", ["x509_test_grain"], minion_tgt="sub_minion")
  92. self.run_function("grains.delkey", ["x509_test_grain"], minion_tgt="minion")
  93. def run_function(self, *args, **kwargs): # pylint: disable=arguments-differ
  94. ret = super().run_function(*args, **kwargs)
  95. return ret
  96. @staticmethod
  97. def file_checksum(path):
  98. hash = hashlib.sha1()
  99. with salt.utils.files.fopen(path, "rb") as f:
  100. for block in iter(lambda: f.read(4096), b""):
  101. hash.update(block)
  102. return hash.hexdigest()
  103. @with_tempfile(suffix=".pem", create=False)
  104. @slowTest
  105. def test_issue_49027(self, pemfile):
  106. ret = self.run_state("x509.pem_managed", name=pemfile, text=self.x509_cert_text)
  107. assert isinstance(ret, dict), ret
  108. ret = ret[next(iter(ret))]
  109. assert ret.get("result") is True, ret
  110. with salt.utils.files.fopen(pemfile) as fp:
  111. result = fp.readlines()
  112. self.assertEqual(self.x509_cert_text.splitlines(True), result)
  113. @with_tempfile(suffix=".crt", create=False)
  114. @with_tempfile(suffix=".key", create=False)
  115. @slowTest
  116. def test_issue_49008(self, keyfile, crtfile):
  117. ret = self.run_function(
  118. "state.apply",
  119. ["issue-49008"],
  120. pillar={"keyfile": keyfile, "crtfile": crtfile},
  121. )
  122. assert isinstance(ret, dict), ret
  123. for state_result in ret.values():
  124. assert state_result["result"] is True, state_result
  125. assert os.path.exists(keyfile)
  126. assert os.path.exists(crtfile)
  127. @slowTest
  128. def test_cert_signing(self):
  129. ret = self.run_function(
  130. "state.apply", ["x509.cert_signing"], pillar={"tmp_dir": RUNTIME_VARS.TMP}
  131. )
  132. key = "x509_|-test_crt_|-{}/pki/test.crt_|-certificate_managed".format(
  133. RUNTIME_VARS.TMP
  134. )
  135. assert key in ret
  136. assert "changes" in ret[key]
  137. assert "Certificate" in ret[key]["changes"]
  138. assert "New" in ret[key]["changes"]["Certificate"]
  139. @slowTest
  140. def test_cert_signing_based_on_csr(self):
  141. ret = self.run_function(
  142. "state.apply",
  143. ["x509.cert_signing_based_on_csr"],
  144. pillar={"tmp_dir": RUNTIME_VARS.TMP},
  145. )
  146. key = "x509_|-test_crt_|-{}/pki/test.crt_|-certificate_managed".format(
  147. RUNTIME_VARS.TMP
  148. )
  149. assert key in ret
  150. assert "changes" in ret[key]
  151. assert "Certificate" in ret[key]["changes"]
  152. assert "New" in ret[key]["changes"]["Certificate"]
  153. @slowTest
  154. def test_crl_managed(self):
  155. ret = self.run_function(
  156. "state.apply", ["x509.crl_managed"], pillar={"tmp_dir": RUNTIME_VARS.TMP}
  157. )
  158. key = "x509_|-{}/pki/ca.crl_|-{}/pki/ca.crl_|-crl_managed".format(
  159. RUNTIME_VARS.TMP, RUNTIME_VARS.TMP
  160. )
  161. # hints for easier debugging
  162. # import json
  163. # print(json.dumps(ret[key], indent=4, sort_keys=True))
  164. # print(ret[key]['comment'])
  165. assert key in ret
  166. assert "changes" in ret[key]
  167. self.assertEqual(ret[key]["result"], True)
  168. assert "New" in ret[key]["changes"]
  169. assert "Revoked Certificates" in ret[key]["changes"]["New"]
  170. self.assertEqual(
  171. ret[key]["changes"]["Old"],
  172. "{}/pki/ca.crl does not exist.".format(RUNTIME_VARS.TMP),
  173. )
  174. @slowTest
  175. def test_crl_managed_replacing_existing_crl(self):
  176. os.mkdir(os.path.join(RUNTIME_VARS.TMP, "pki"))
  177. with salt.utils.files.fopen(
  178. os.path.join(RUNTIME_VARS.TMP, "pki/ca.crl"), "wb"
  179. ) as crl_file:
  180. crl_file.write(
  181. b"""-----BEGIN RSA PRIVATE KEY-----
  182. MIICWwIBAAKBgQCjdjbgL4kQ8Lu73xeRRM1q3C3K3ptfCLpyfw38LRnymxaoJ6ls
  183. pNSx2dU1uJ89YKFlYLo1QcEk4rJ2fdIjarV0kuNCY3rC8jYUp9BpAU5Z6p9HKeT1
  184. 2rTPH81JyjbQDR5PyfCyzYOQtpwpB4zIUUK/Go7tTm409xGKbbUFugJNgQIDAQAB
  185. AoGAF24we34U1ZrMLifSRv5nu3OIFNZHyx2DLDpOFOGaII5edwgIXwxZeIzS5Ppr
  186. yO568/8jcdLVDqZ4EkgCwRTgoXRq3a1GLHGFmBdDNvWjSTTMLoozuM0t2zjRmIsH
  187. hUd7tnai9Lf1Bp5HlBEhBU2gZWk+SXqLvxXe74/+BDAj7gECQQDRw1OPsrgTvs3R
  188. 3MNwX6W8+iBYMTGjn6f/6rvEzUs/k6rwJluV7n8ISNUIAxoPy5g5vEYK6Ln/Ttc7
  189. u0K1KNlRAkEAx34qcxjuswavL3biNGE+8LpDJnJx1jaNWoH+ObuzYCCVMusdT2gy
  190. kKuq9ytTDgXd2qwZpIDNmscvReFy10glMQJAXebMz3U4Bk7SIHJtYy7OKQzn0dMj
  191. 35WnRV81c2Jbnzhhu2PQeAvt/i1sgEuzLQL9QEtSJ6wLJ4mJvImV0TdaIQJAAYyk
  192. TcKK0A8kOy0kMp3yvDHmJZ1L7wr7bBGIZPBlQ0Ddh8i1sJExm1gJ+uN2QKyg/XrK
  193. tDFf52zWnCdVGgDwcQJALW/WcbSEK+JVV6KDJYpwCzWpKIKpBI0F6fdCr1G7Xcwj
  194. c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ==
  195. -----END RSA PRIVATE KEY-----
  196. """
  197. )
  198. ret = self.run_function(
  199. "state.apply", ["x509.crl_managed"], pillar={"tmp_dir": RUNTIME_VARS.TMP}
  200. )
  201. key = "x509_|-{}/pki/ca.crl_|-{}/pki/ca.crl_|-crl_managed".format(
  202. RUNTIME_VARS.TMP, RUNTIME_VARS.TMP
  203. )
  204. # hints for easier debugging
  205. # import json
  206. # print(json.dumps(ret[key], indent=4, sort_keys=True))
  207. # print(ret[key]['comment'])
  208. assert key in ret
  209. assert "changes" in ret[key]
  210. self.assertEqual(ret[key]["result"], True)
  211. assert "New" in ret[key]["changes"]
  212. assert "Revoked Certificates" in ret[key]["changes"]["New"]
  213. self.assertEqual(
  214. ret[key]["changes"]["Old"],
  215. "{}/pki/ca.crl is not a valid CRL.".format(RUNTIME_VARS.TMP),
  216. )
  217. def test_cert_issue_not_before_not_after(self):
  218. ret = self.run_function(
  219. "state.apply",
  220. ["test_cert_not_before_not_after"],
  221. pillar={"tmp_dir": RUNTIME_VARS.TMP},
  222. )
  223. key = "x509_|-test_crt_|-{}/pki/test.crt_|-certificate_managed".format(
  224. RUNTIME_VARS.TMP
  225. )
  226. assert key in ret
  227. assert "changes" in ret[key]
  228. assert "Certificate" in ret[key]["changes"]
  229. assert "New" in ret[key]["changes"]["Certificate"]
  230. assert "Not Before" in ret[key]["changes"]["Certificate"]["New"]
  231. assert "Not After" in ret[key]["changes"]["Certificate"]["New"]
  232. not_before = ret[key]["changes"]["Certificate"]["New"]["Not Before"]
  233. not_after = ret[key]["changes"]["Certificate"]["New"]["Not After"]
  234. assert not_before == "2019-05-05 00:00:00"
  235. assert not_after == "2020-05-05 14:30:00"
  236. def test_cert_issue_not_before(self):
  237. ret = self.run_function(
  238. "state.apply",
  239. ["test_cert_not_before"],
  240. pillar={"tmp_dir": RUNTIME_VARS.TMP},
  241. )
  242. key = "x509_|-test_crt_|-{}/pki/test.crt_|-certificate_managed".format(
  243. RUNTIME_VARS.TMP
  244. )
  245. assert key in ret
  246. assert "changes" in ret[key]
  247. assert "Certificate" in ret[key]["changes"]
  248. assert "New" in ret[key]["changes"]["Certificate"]
  249. assert "Not Before" in ret[key]["changes"]["Certificate"]["New"]
  250. assert "Not After" in ret[key]["changes"]["Certificate"]["New"]
  251. not_before = ret[key]["changes"]["Certificate"]["New"]["Not Before"]
  252. assert not_before == "2019-05-05 00:00:00"
  253. def test_cert_issue_not_after(self):
  254. ret = self.run_function(
  255. "state.apply", ["test_cert_not_after"], pillar={"tmp_dir": RUNTIME_VARS.TMP}
  256. )
  257. key = "x509_|-test_crt_|-{}/pki/test.crt_|-certificate_managed".format(
  258. RUNTIME_VARS.TMP
  259. )
  260. assert key in ret
  261. assert "changes" in ret[key]
  262. assert "Certificate" in ret[key]["changes"]
  263. assert "New" in ret[key]["changes"]["Certificate"]
  264. assert "Not Before" in ret[key]["changes"]["Certificate"]["New"]
  265. assert "Not After" in ret[key]["changes"]["Certificate"]["New"]
  266. not_after = ret[key]["changes"]["Certificate"]["New"]["Not After"]
  267. assert not_after == "2020-05-05 14:30:00"
  268. @with_tempfile(suffix=".crt", create=False)
  269. @with_tempfile(suffix=".key", create=False)
  270. def test_issue_41858(self, keyfile, crtfile):
  271. ret_key = "x509_|-test_crt_|-{}_|-certificate_managed".format(crtfile)
  272. signing_policy = "no_such_policy"
  273. ret = self.run_function(
  274. "state.apply",
  275. ["issue-41858.gen_cert"],
  276. pillar={
  277. "keyfile": keyfile,
  278. "crtfile": crtfile,
  279. "tmp_dir": RUNTIME_VARS.TMP,
  280. },
  281. )
  282. self.assertTrue(ret[ret_key]["result"])
  283. cert_sum = self.file_checksum(crtfile)
  284. ret = self.run_function(
  285. "state.apply",
  286. ["issue-41858.check"],
  287. pillar={
  288. "keyfile": keyfile,
  289. "crtfile": crtfile,
  290. "signing_policy": signing_policy,
  291. },
  292. )
  293. self.assertFalse(ret[ret_key]["result"])
  294. # self.assertSaltCommentRegexpMatches(ret[ret_key], "Signing policy {0} does not exist".format(signing_policy))
  295. self.assertEqual(self.file_checksum(crtfile), cert_sum)
  296. @with_tempfile(suffix=".crt", create=False)
  297. @with_tempfile(suffix=".key", create=False)
  298. def test_compound_match_minion_have_correct_grain_value(self, keyfile, crtfile):
  299. ret_key = "x509_|-test_crt_|-{}_|-certificate_managed".format(crtfile)
  300. signing_policy = "compound_match"
  301. ret = self.run_function(
  302. "state.apply",
  303. ["x509_compound_match.gen_ca"],
  304. pillar={"tmp_dir": RUNTIME_VARS.TMP},
  305. )
  306. # sub_minion have grain set and CA is on other minion
  307. # CA minion have same grain with incorrect value
  308. ret = self.run_function(
  309. "state.apply",
  310. ["x509_compound_match.check"],
  311. minion_tgt="sub_minion",
  312. pillar={
  313. "keyfile": keyfile,
  314. "crtfile": crtfile,
  315. "signing_policy": signing_policy,
  316. },
  317. )
  318. self.assertTrue(ret[ret_key]["result"])
  319. @with_tempfile(suffix=".crt", create=False)
  320. @with_tempfile(suffix=".key", create=False)
  321. def test_compound_match_ca_have_correct_grain_value(self, keyfile, crtfile):
  322. self.run_function(
  323. "grains.set", ["x509_test_grain", "correct_value"], minion_tgt="minion"
  324. )
  325. self.run_function(
  326. "grains.set",
  327. ["x509_test_grain", "not_correct_value"],
  328. minion_tgt="sub_minion",
  329. )
  330. ret_key = "x509_|-test_crt_|-{}_|-certificate_managed".format(crtfile)
  331. signing_policy = "compound_match"
  332. self.run_function(
  333. "state.apply",
  334. ["x509_compound_match.gen_ca"],
  335. pillar={"tmp_dir": RUNTIME_VARS.TMP},
  336. )
  337. ret = self.run_function(
  338. "state.apply",
  339. ["x509_compound_match.check"],
  340. minion_tgt="sub_minion",
  341. pillar={
  342. "keyfile": keyfile,
  343. "crtfile": crtfile,
  344. "signing_policy": signing_policy,
  345. },
  346. )
  347. self.assertFalse(ret[ret_key]["result"])
  348. @with_tempfile(suffix=".crt", create=False)
  349. @with_tempfile(suffix=".key", create=False)
  350. def test_self_signed_cert(self, keyfile, crtfile):
  351. """
  352. Self-signed certificate, no CA.
  353. Run the state twice to confirm the cert is only created once
  354. and its contents don't change.
  355. """
  356. first_run = self.run_function(
  357. "state.apply",
  358. ["x509.self_signed"],
  359. pillar={"keyfile": keyfile, "crtfile": crtfile},
  360. )
  361. key = "x509_|-self_signed_cert_|-{}_|-certificate_managed".format(crtfile)
  362. self.assertIn("New", first_run[key]["changes"]["Certificate"])
  363. self.assertEqual(
  364. "Certificate is valid and up to date",
  365. first_run[key]["changes"]["Status"]["New"],
  366. )
  367. self.assertTrue(os.path.exists(crtfile), "Certificate was not created.")
  368. with salt.utils.files.fopen(crtfile, "r") as first_cert:
  369. cert_contents = first_cert.read()
  370. second_run = self.run_function(
  371. "state.apply",
  372. ["x509.self_signed"],
  373. pillar={"keyfile": keyfile, "crtfile": crtfile},
  374. )
  375. self.assertEqual({}, second_run[key]["changes"])
  376. with salt.utils.files.fopen(crtfile, "r") as second_cert:
  377. self.assertEqual(
  378. cert_contents,
  379. second_cert.read(),
  380. "Certificate contents should not have changed.",
  381. )
  382. @with_tempfile(suffix=".crt", create=False)
  383. @with_tempfile(suffix=".key", create=False)
  384. def test_old_self_signed_cert_is_recreated(self, keyfile, crtfile):
  385. """
  386. Self-signed certificate, no CA.
  387. First create a cert that expires in 30 days, then recreate
  388. the cert because the second state run requires days_remaining
  389. to be at least 90.
  390. """
  391. first_run = self.run_function(
  392. "state.apply",
  393. ["x509.self_signed_expiry"],
  394. pillar={
  395. "keyfile": keyfile,
  396. "crtfile": crtfile,
  397. "days_valid": 30,
  398. "days_remaining": 10,
  399. },
  400. )
  401. key = "x509_|-self_signed_cert_|-{}_|-certificate_managed".format(crtfile)
  402. self.assertEqual(
  403. "Certificate is valid and up to date",
  404. first_run[key]["changes"]["Status"]["New"],
  405. )
  406. expiry = datetime.datetime.strptime(
  407. first_run[key]["changes"]["Certificate"]["New"]["Not After"],
  408. "%Y-%m-%d %H:%M:%S",
  409. )
  410. self.assertEqual(29, (expiry - datetime.datetime.now()).days)
  411. self.assertTrue(os.path.exists(crtfile), "Certificate was not created.")
  412. with salt.utils.files.fopen(crtfile, "r") as first_cert:
  413. cert_contents = first_cert.read()
  414. second_run = self.run_function(
  415. "state.apply",
  416. ["x509.self_signed_expiry"],
  417. pillar={
  418. "keyfile": keyfile,
  419. "crtfile": crtfile,
  420. "days_valid": 180,
  421. "days_remaining": 90,
  422. },
  423. )
  424. self.assertEqual(
  425. "Certificate needs renewal: 29 days remaining but it needs to be at least 90",
  426. second_run[key]["changes"]["Status"]["Old"],
  427. )
  428. expiry = datetime.datetime.strptime(
  429. second_run[key]["changes"]["Certificate"]["New"]["Not After"],
  430. "%Y-%m-%d %H:%M:%S",
  431. )
  432. self.assertEqual(179, (expiry - datetime.datetime.now()).days)
  433. with salt.utils.files.fopen(crtfile, "r") as second_cert:
  434. self.assertNotEqual(
  435. cert_contents,
  436. second_cert.read(),
  437. "Certificate contents should have changed.",
  438. )
  439. @with_tempfile(suffix=".crt", create=False)
  440. @with_tempfile(suffix=".key", create=False)
  441. def test_mismatched_self_signed_cert_is_recreated(self, keyfile, crtfile):
  442. """
  443. Self-signed certificate, no CA.
  444. First create a cert, then run the state again with a different
  445. subjectAltName. The cert should be recreated.
  446. Finally, run once more with the same subjectAltName as the
  447. second run. Nothing should change.
  448. """
  449. first_run = self.run_function(
  450. "state.apply",
  451. ["x509.self_signed_different_properties"],
  452. pillar={
  453. "keyfile": keyfile,
  454. "crtfile": crtfile,
  455. "subjectAltName": "DNS:alt.service.local",
  456. },
  457. )
  458. key = "x509_|-self_signed_cert_|-{}_|-certificate_managed".format(crtfile)
  459. self.assertEqual(
  460. "Certificate is valid and up to date",
  461. first_run[key]["changes"]["Status"]["New"],
  462. )
  463. sans = first_run[key]["changes"]["Certificate"]["New"]["X509v3 Extensions"][
  464. "subjectAltName"
  465. ]
  466. self.assertEqual("DNS:alt.service.local", sans)
  467. self.assertTrue(os.path.exists(crtfile), "Certificate was not created.")
  468. with salt.utils.files.fopen(crtfile, "r") as first_cert:
  469. first_cert_contents = first_cert.read()
  470. second_run_pillar = {
  471. "keyfile": keyfile,
  472. "crtfile": crtfile,
  473. "subjectAltName": "DNS:alt1.service.local, DNS:alt2.service.local",
  474. }
  475. second_run = self.run_function(
  476. "state.apply",
  477. ["x509.self_signed_different_properties"],
  478. pillar=second_run_pillar,
  479. )
  480. self.assertEqual(
  481. "Certificate properties are different: X509v3 Extensions",
  482. second_run[key]["changes"]["Status"]["Old"],
  483. )
  484. sans = second_run[key]["changes"]["Certificate"]["New"]["X509v3 Extensions"][
  485. "subjectAltName"
  486. ]
  487. self.assertEqual("DNS:alt1.service.local, DNS:alt2.service.local", sans)
  488. with salt.utils.files.fopen(crtfile, "r") as second_cert:
  489. second_cert_contents = second_cert.read()
  490. self.assertNotEqual(
  491. first_cert_contents,
  492. second_cert_contents,
  493. "Certificate contents should have changed.",
  494. )
  495. third_run = self.run_function(
  496. "state.apply",
  497. ["x509.self_signed_different_properties"],
  498. pillar=second_run_pillar,
  499. )
  500. self.assertEqual({}, third_run[key]["changes"])
  501. with salt.utils.files.fopen(crtfile, "r") as third_cert:
  502. self.assertEqual(
  503. second_cert_contents,
  504. third_cert.read(),
  505. "Certificate contents should not have changed.",
  506. )
  507. @with_tempfile(suffix=".crt", create=False)
  508. @with_tempfile(suffix=".key", create=False)
  509. def test_certificate_managed_with_managed_private_key_does_not_error(
  510. self, keyfile, crtfile
  511. ):
  512. """
  513. Test using the deprecated managed_private_key arg in certificate_managed does not throw an error.
  514. TODO: Remove this test in Aluminium when the arg is removed.
  515. """
  516. self.run_state("x509.private_key_managed", name=keyfile, bits=4096)
  517. ret = self.run_state(
  518. "x509.certificate_managed",
  519. name=crtfile,
  520. CN="localhost",
  521. signing_private_key=keyfile,
  522. managed_private_key={"name": keyfile, "bits": 4096},
  523. )
  524. key = "x509_|-{0}_|-{0}_|-certificate_managed".format(crtfile)
  525. self.assertEqual(True, ret[key]["result"])
  526. @with_tempfile(suffix=".crt", create=False)
  527. @with_tempfile(suffix=".key", create=False)
  528. def test_file_properties_are_updated(self, keyfile, crtfile):
  529. """
  530. Self-signed certificate, no CA.
  531. First create a cert, then run the state again with different
  532. file mode. The cert should not be recreated, but the file
  533. should be updated.
  534. Finally, run once more with the same file mode as the second
  535. run. Nothing should change.
  536. """
  537. first_run = self.run_function(
  538. "state.apply",
  539. ["x509.self_signed_different_properties"],
  540. pillar={"keyfile": keyfile, "crtfile": crtfile, "fileMode": "0755"},
  541. )
  542. key = "x509_|-self_signed_cert_|-{}_|-certificate_managed".format(crtfile)
  543. self.assertEqual(
  544. "Certificate is valid and up to date",
  545. first_run[key]["changes"]["Status"]["New"],
  546. )
  547. self.assertTrue(os.path.exists(crtfile), "Certificate was not created.")
  548. self.assertEqual("0755", oct(os.stat(crtfile).st_mode)[-4:])
  549. second_run_pillar = {
  550. "keyfile": keyfile,
  551. "crtfile": crtfile,
  552. "mode": "0600",
  553. }
  554. second_run = self.run_function(
  555. "state.apply",
  556. ["x509.self_signed_different_properties"],
  557. pillar=second_run_pillar,
  558. )
  559. self.assertEqual("0600", oct(os.stat(crtfile).st_mode)[-4:])
  560. third_run = self.run_function(
  561. "state.apply",
  562. ["x509.self_signed_different_properties"],
  563. pillar=second_run_pillar,
  564. )
  565. self.assertEqual({}, third_run[key]["changes"])
  566. self.assertEqual("0600", oct(os.stat(crtfile).st_mode)[-4:])
  567. @with_tempfile(suffix=".crt", create=False)
  568. @with_tempfile(suffix=".key", create=False)
  569. def test_file_managed_failure(self, keyfile, crtfile):
  570. """
  571. Test that a failure in the file.managed call marks the state
  572. call as failed.
  573. """
  574. crtfile_pieces = os.path.split(crtfile)
  575. bad_crtfile = os.path.join(
  576. crtfile_pieces[0], "deeply/nested", crtfile_pieces[1]
  577. )
  578. ret = self.run_function(
  579. "state.apply",
  580. ["x509.self_signed_file_error"],
  581. pillar={"keyfile": keyfile, "crtfile": bad_crtfile},
  582. )
  583. key = "x509_|-self_signed_cert_|-{}_|-certificate_managed".format(bad_crtfile)
  584. self.assertFalse(ret[key]["result"], "State should have failed.")
  585. self.assertEqual({}, ret[key]["changes"])
  586. self.assertFalse(
  587. os.path.exists(crtfile), "Certificate should not have been created."
  588. )
  589. @with_tempfile(suffix=".crt", create=False)
  590. @with_tempfile(suffix=".key", create=False)
  591. def test_py2_generated_cert_is_not_recreated(self, keyfile, crtfile):
  592. keyfile_contents = textwrap.dedent(
  593. """\
  594. -----BEGIN RSA PRIVATE KEY-----
  595. MIIEpAIBAAKCAQEAp5PQyx5NlYrfzd7vU/Xb2YR5qbWWtpWWoKmJC1gML5v5DBI7
  596. +p/kAHNNmK8uqHXTaI4N/zgarfjrg4zceq2Du7pP0xiCAYolhFqF78ibxNrN4OkT
  597. UPm2kM88iJ8Z14Yph8ueSxLIlujCGaEFhr6wRzTj4T9b+0Bb/PZHI2t5YwtIooVM
  598. EFCBFkt4bb004tO0D9q0CPPVT2AsGmxnY43Aj3Epy++kqmaWj1hIucSprkDrAXFS
  599. WacBQPFQ8XctnL2Z1Q6CJ5WUNrW8ohAJ9RJkwjiqbZTwYIPSSrl+FO3XqDY70SxU
  600. 3xDeqhU4zvyjxJ8w9SPqTUu/C3BZtRBT9dCBEQIDAQABAoIBAQCZvS23u1RYVrEe
  601. sWGF+LA67aOkg9kCJ1iqiv8UrjF32DNy1KO8OcY2d5H/+u/mUzqh2HmU5QbtBsoi
  602. xS9dSSTrLHGhbAGRogjrVRU9uCDYSBjLN2mmR4IrdkTF3pkZtpcRY0gU/eWTNXUl
  603. iCmGxhj5KtfJxZQAfLon6FW5dBdIOgxSCJhvRq0zFpWJZFGWWkBExDfeNg//0fCU
  604. UbjRjGacP/+R6FSJa6tevzgR7tIIapm1dY/ofPXIXsZGo1R87fRgLI1D+e84Jdds
  605. /U0bKzPOgAjcC1b262lJ8058pjG/nqWC0YUfpIJUVv2ciJpH3Ha+90526InLAUXA
  606. RWe1Z2YxAoGBANqACEKvUbxENu+XxQj0SI1co4SRTOvgbrSQGL61rDY6PvY/bOqC
  607. JeR0KC3MN6e7fx52tsl/eqP9iyExUpO9b0BCnGg967MivJXWUxhUdOL/r2ceQBqD
  608. DiPVZCFsjeNdSNihnNctAig9Po3GEUWE0ikHr3NcD+wXTnhnIEjJ/fltAoGBAMRW
  609. dIcOiuDLm/oDLNCpwEO4m63ymbUgeOj2cZhKMTqFmspnKnuCU1U/A8cuQcs1gydL
  610. 7MzxVP7MZDIEqT5gGj3eyuVMAmKbvLFR2NctDIDjaUs6oz0J9NGByPNjXaYr4uMd
  611. EZrxD8gLZ/G+/7eKsCgBA9ksSydDo00Vf/qAsmO1AoGBANWqc+l59eyrrCj5egU6
  612. lKQf3gsp51WV/8v0SS5dC41vwdgdx80+/fz8FbpLRHVypWlN34sFbRFmQ6Juz/iH
  613. O35UZQyO2KkxI8dGcbWOCUtditHExBzo4W/rIWKJ++pFc5Hb4DqO2dgto7kR4hvg
  614. OX9D869UbIGLfQHCntBvLju1AoGAHpcl0sEmTD4NEFgcTGqWZTbHMsQAxOLJU+rJ
  615. 6iNtJiQY6P5H9TRqDXci/I6te57bz2yZ+ZiEWKq51b06LVjF3evviuhb2sdPEAWj
  616. lmsTbqWAC1OYiXMarOXezGUn+zMNR7uIua5jehSk3lqW9x7psWHvGpA3KWf1cpYt
  617. +XbB1J0CgYBCSjALTv4dcn+CtS3kqb806z8H9MSZznUwSmcgvwCR5sqwLAUk1xRn
  618. hEqXbC1RGee3Xqv9mXPDK2LirpdRYi9Jr9ApZkrSkeaXSd2d4cy2ujUT0c7P8JrD
  619. i6QXb+HaFeBuS5ulYDmo4mIbCysuTsgrLzplViUy3xUQv23M/Eh1gw==
  620. -----END RSA PRIVATE KEY-----
  621. """
  622. )
  623. crtfile_contents = textwrap.dedent(
  624. """\
  625. -----BEGIN CERTIFICATE-----
  626. MIIEhTCCA22gAwIBAgIIUijHgif6VJUwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV
  627. BAYTAkJFMRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExETAPBgNVBAcMCEthcGVs
  628. bGVuMRAwDgYDVQQIDAdBbnR3ZXJwMRAwDgYDVQQKDAdFeGFtcGxlMSIwIAYJKoZI
  629. hvcNAQkBFhNjZXJ0YWRtQGV4YW1wbGUub3JnMB4XDTIwMDYxNjA3Mzk1OVoXDTMw
  630. MDYxNDA3Mzk1OVowgYIxCzAJBgNVBAYTAkJFMRgwFgYDVQQDDA9FeGFtcGxlIFJv
  631. b3QgQ0ExETAPBgNVBAcMCEthcGVsbGVuMRAwDgYDVQQIDAdBbnR3ZXJwMRAwDgYD
  632. VQQKDAdFeGFtcGxlMSIwIAYJKoZIhvcNAQkBFhNjZXJ0YWRtQGV4YW1wbGUub3Jn
  633. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp5PQyx5NlYrfzd7vU/Xb
  634. 2YR5qbWWtpWWoKmJC1gML5v5DBI7+p/kAHNNmK8uqHXTaI4N/zgarfjrg4zceq2D
  635. u7pP0xiCAYolhFqF78ibxNrN4OkTUPm2kM88iJ8Z14Yph8ueSxLIlujCGaEFhr6w
  636. RzTj4T9b+0Bb/PZHI2t5YwtIooVMEFCBFkt4bb004tO0D9q0CPPVT2AsGmxnY43A
  637. j3Epy++kqmaWj1hIucSprkDrAXFSWacBQPFQ8XctnL2Z1Q6CJ5WUNrW8ohAJ9RJk
  638. wjiqbZTwYIPSSrl+FO3XqDY70SxU3xDeqhU4zvyjxJ8w9SPqTUu/C3BZtRBT9dCB
  639. EQIDAQABo4H8MIH5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
  640. A1UdDgQWBBTmNsYLuQTxpANgTuw7LRn1qHJsjzCBtgYDVR0jBIGuMIGrgBTmNsYL
  641. uQTxpANgTuw7LRn1qHJsj6GBiKSBhTCBgjELMAkGA1UEBhMCQkUxGDAWBgNVBAMM
  642. D0V4YW1wbGUgUm9vdCBDQTERMA8GA1UEBwwIS2FwZWxsZW4xEDAOBgNVBAgMB0Fu
  643. dHdlcnAxEDAOBgNVBAoMB0V4YW1wbGUxIjAgBgkqhkiG9w0BCQEWE2NlcnRhZG1A
  644. ZXhhbXBsZS5vcmeCCFIox4In+lSVMA0GCSqGSIb3DQEBCwUAA4IBAQBnC1/kK+xr
  645. Vjr5Y2YRjyjm4e8I/nTU+RX2p5K+Yth3CqWO3JuDiV/31UMtPl832n2GWSgXG2pP
  646. B52oeuCP4Re76jqhOmJWY3CKPji+Rs16wj199i9AAcwhSF0rpi5+Fi84HtP3q6pH
  647. cuzZfIPW44aJ5l4k+QvTLoWzr0XujMFcYzI45i3SJqTMs8xdIP5YLN8JXtQSPw9Z
  648. 8/nBKbPj7WTUC9cj9Cw2bz+wTpdRF4XCsUF3Vpl9fP7SK8yvv0I85LZnWQx1eQlv
  649. COAM5HWxUT9bWgv18zXdYkc6VLw6ufQSxxuhLMjJxuK27Ny/F18/xYLRTVnse36d
  650. tPJrseUPmvIK
  651. -----END CERTIFICATE-----
  652. """
  653. )
  654. slsfile = textwrap.dedent(
  655. """\
  656. {%- set ca_key_path = '"""
  657. + keyfile
  658. + """' %}
  659. {%- set ca_crt_path = '"""
  660. + crtfile
  661. + """' %}
  662. certificate.authority::private-key:
  663. x509.private_key_managed:
  664. - name: {{ ca_key_path }}
  665. - backup: True
  666. certificate.authority::certificate:
  667. x509.certificate_managed:
  668. - name: {{ ca_crt_path }}
  669. - signing_private_key: {{ ca_key_path }}
  670. - CN: Example Root CA
  671. - O: Example
  672. - C: BE
  673. - ST: Antwerp
  674. - L: Kapellen
  675. - Email: certadm@example.org
  676. - basicConstraints: "critical CA:true"
  677. - keyUsage: "critical cRLSign, keyCertSign"
  678. - subjectKeyIdentifier: hash
  679. - authorityKeyIdentifier: keyid,issuer:always
  680. - days_valid: 3650
  681. - days_remaining: 0
  682. - backup: True
  683. - require:
  684. - x509: certificate.authority::private-key
  685. """
  686. )
  687. with salt.utils.files.fopen(
  688. os.path.join(RUNTIME_VARS.TMP_STATE_TREE, "cert.sls"), "w"
  689. ) as wfh:
  690. wfh.write(slsfile)
  691. # Generate the certificate twice.
  692. # On the first run, no key nor cert exist.
  693. ret = self.run_function("state.sls", ["cert"])
  694. log.debug(
  695. "First state run ret dictionary:\n%s", pprint.pformat(list(ret.values()))
  696. )
  697. for state_run_id, state_run_details in ret.items():
  698. if state_run_id.endswith("private_key_managed"):
  699. assert state_run_details["result"]
  700. assert "new" in state_run_details["changes"]
  701. if state_run_id.endswith("certificate_managed"):
  702. assert state_run_details["result"]
  703. assert "Certificate" in state_run_details["changes"]
  704. assert "New" in state_run_details["changes"]["Certificate"]
  705. assert "Status" in state_run_details["changes"]
  706. assert "New" in state_run_details["changes"]["Status"]
  707. # On the second run, they exist and should not trigger any modification
  708. ret = self.run_function("state.sls", ["cert"])
  709. log.debug(
  710. "Second state run ret dictionary:\n%s", pprint.pformat(list(ret.values()))
  711. )
  712. for state_run_id, state_run_details in ret.items():
  713. if state_run_id.endswith("private_key_managed"):
  714. assert state_run_details["result"]
  715. assert state_run_details["changes"] == {}
  716. if state_run_id.endswith("certificate_managed"):
  717. assert state_run_details["result"]
  718. assert state_run_details["changes"] == {}
  719. # Now we repleace they key and cert contents with the contents of the above
  720. # call, but under Py2
  721. with salt.utils.files.fopen(keyfile, "w") as wfh:
  722. wfh.write(keyfile_contents)
  723. with salt.utils.files.fopen(keyfile) as rfh:
  724. log.debug("Written keyfile, %r, contents:\n%s", keyfile, rfh.read())
  725. with salt.utils.files.fopen(crtfile, "w") as wfh:
  726. wfh.write(crtfile_contents)
  727. with salt.utils.files.fopen(crtfile) as rfh:
  728. log.debug("Written crtfile, %r, contents:\n%s", crtfile, rfh.read())
  729. # We should not trigger any modification
  730. ret = self.run_function("state.sls", ["cert"])
  731. log.debug(
  732. "Third state run ret dictionary:\n%s", pprint.pformat(list(ret.values()))
  733. )
  734. for state_run_id, state_run_details in ret.items():
  735. if state_run_id.endswith("private_key_managed"):
  736. assert state_run_details["result"]
  737. assert state_run_details["changes"] == {}
  738. if state_run_id.endswith("certificate_managed"):
  739. assert state_run_details["result"]
  740. assert state_run_details["changes"] == {}