From 6ca45226e27fa8407262bd819fb837d0042ec774 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 12 Nov 2013 15:09:36 -0500 Subject: testsuite: Wrote bcfg2-crypt end-to-end tests --- testsuite/Testsrc/Testsbin/test_bcfg2_crypt.py | 390 +++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 testsuite/Testsrc/Testsbin/test_bcfg2_crypt.py (limited to 'testsuite/Testsrc/Testsbin/test_bcfg2_crypt.py') diff --git a/testsuite/Testsrc/Testsbin/test_bcfg2_crypt.py b/testsuite/Testsrc/Testsbin/test_bcfg2_crypt.py new file mode 100644 index 000000000..3eee4415f --- /dev/null +++ b/testsuite/Testsrc/Testsbin/test_bcfg2_crypt.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- +import os +import sys +import shutil +import difflib +import tempfile +import lxml.etree +import Bcfg2.Options +from Bcfg2.Compat import StringIO, b64decode, u_str +from mock import Mock, MagicMock, patch + +# add all parent testsuite directories to sys.path to allow (most) +# relative imports in python 2.4 +path = os.path.dirname(__file__) +while path != "/": + if os.path.basename(path).lower().startswith("test"): + sys.path.append(path) + if os.path.basename(path) == "testsuite": + break + path = os.path.dirname(path) +from common import * + +try: + from Bcfg2.Server.Encryption import CLI + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + + +class TestEncryption(Bcfg2TestCase): + cfg_plaintext = None + known_files = None + basedir = None + + @classmethod + def setUpClass(cls): + basedir = os.path.join(os.path.dirname(__file__), "bcfg2-crypt") + cls.basedir = tempfile.mkdtemp() + for fname in os.listdir(basedir): + shutil.copy(os.path.join(basedir, fname), cls.basedir) + cls.known_files = os.listdir(cls.basedir) + cls.cfg_plaintext = open(os.path.join(cls.basedir, "plaintext")).read() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.basedir) + + @skipUnless(HAS_CRYPTO, "Encryption libraries not found") + def setUp(self): + set_setup_default("lax_decryption", False) + + def set_options(self): + Bcfg2.Options.setup.algorithm = "aes_256_cbc" + Bcfg2.Options.setup.passphrases = dict( + basic="basic", + complex="1234567890əùíÿł¢€ñû⸘" * 10) + + def tearDown(self): + # clean up stray files created by tests + for fname in os.listdir(self.basedir): + if fname not in self.known_files: + os.unlink(os.path.join(self.basedir, fname)) + + def assertExists(self, fname): + fpath = os.path.join(self.basedir, fname) + self.assertTrue(os.path.exists(fpath), + "%s does not exist" % fpath) + + def assertNotExists(self, fname): + fpath = os.path.join(self.basedir, fname) + self.assertFalse(os.path.exists(fpath), + "%s exists, but shouldn't" % fpath) + + def assertFilesEqual(self, fname1, fname2): + self.assertExists(fname1) + self.assertExists(fname2) + contents1 = open(os.path.join(self.basedir, fname1)).read().strip() + contents2 = open(os.path.join(self.basedir, fname2)).read().strip() + diff = "\n".join( + difflib.unified_diff(contents1.splitlines(), + contents2.splitlines(), + fname1, fname2)).replace("\n\n", "\n") + self.assertEqual(contents1, contents2, + "Contents of %s and %s do not match:\n%s" % + (fname1, fname2, diff)) + + def assertFilesNotEqual(self, fname1, fname2): + self.assertExists(fname1) + self.assertExists(fname2) + self.assertNotEqual( + open(os.path.join(self.basedir, fname1)).read(), + open(os.path.join(self.basedir, fname2)).read(), + "Contents of %s and %s are unexpectedly identical") + + def _is_encrypted(self, data): + """ Pretty crappy check for whether or not data is encrypted: + just see if it's a valid base64-encoded string whose contents + start with "Salted__". But without decrypting, which rather + begs the question in a set of crypto unit tests, I'm not sure + how to do a better test.""" + try: + return b64decode(data).startswith("Salted__") + except UnicodeDecodeError: + # decoded base64, resulting value contained non-ASCII text + return True + except TypeError: + # couldn't decode base64 + return False + + def assertIsEncrypted(self, data): + if not self._is_encrypted(data): + self.fail("Data is not encrypted: %s" % data) + + def assertNotEncrypted(self, data): + if self._is_encrypted(data): + self.fail("Data is unexpectedly encrypted: %s" % data) + + def _decrypt(self, cli, outfile, expected=None): + self.set_options() + cli.run() + if expected is None: + self.assertExists(outfile) + actual = open(os.path.join(self.basedir, outfile)).read() + self.assertEqual(self.cfg_plaintext, actual) + self.assertNotEncrypted(actual) + else: + self.assertFilesEqual(outfile, expected) + + def _encrypt(self, cli, outfile, original=None): + self.set_options() + cli.run() + if original is None: + self.assertExists(outfile) + actual = open(os.path.join(self.basedir, outfile)).read() + self.assertNotEqual(self.cfg_plaintext, actual) + self.assertIsEncrypted(actual) + else: + self.assertFilesNotEqual(outfile, original) + + def _cfg_decrypt(self, opts, encrypted): + if encrypted.endswith(".crypt"): + decrypted = encrypted[:-6] + else: + self.fail("Could not determine decrypted filename for %s" % + encrypted) + cli = CLI(opts + [os.path.join(self.basedir, encrypted)]) + self._decrypt(cli, decrypted) + + def _cfg_encrypt(self, opts, plaintext): + cli = CLI(opts + [os.path.join(self.basedir, plaintext)]) + self._encrypt(cli, plaintext + ".crypt") + + def _props_decrypt(self, opts, encrypted, expected): + test = os.path.join(self.basedir, "test.xml") + shutil.copy(os.path.join(self.basedir, encrypted), test) + cli = CLI(opts + [test]) + self._decrypt(cli, "test.xml", expected) + try: + xdata = lxml.etree.parse(test) + except: + self.fail("Could not parse decrypted Properties file: %s" % + sys.exc_info()[1]) + for el in xdata.iter(): + if el.tag is not lxml.etree.Comment and el.text.strip(): + self.assertNotEncrypted(el.text) + + def _props_encrypt(self, opts, plaintext, check_all=True): + test = os.path.join(self.basedir, "test.xml") + shutil.copy(os.path.join(self.basedir, plaintext), test) + cli = CLI(opts + [test]) + self._encrypt(cli, "test.xml", plaintext) + try: + xdata = lxml.etree.parse(test) + except: + self.fail("Could not parse encrypted Properties file: %s" % + sys.exc_info()[1]) + if check_all: + for el in xdata.iter(): + if el.tag is not lxml.etree.Comment and el.text.strip(): + self.assertIsEncrypted(el.text) + + def test_decrypt_cfg(self): + """ Decrypt a Cfg file """ + self._cfg_decrypt(["--decrypt", "--cfg", "-p", "basic"], + "basic.crypt") + + def test_decrypt_cfg_complex(self): + """ Decrypt a Cfg file with a passphrase with special characters """ + self._cfg_decrypt(["--decrypt", "--cfg", "-p", "complex"], + "complex.crypt") + + def test_decrypt_cfg_algorithm(self): + """ Decrypt a Cfg file with a non-default algorithm """ + # this can't be done with self._cfg_decrypt or even + # self._decrypt because we have to set the algorithm after + # other options are set, but before the decrypt is performed + cli = CLI(["--decrypt", "--cfg", "-p", "basic", + os.path.join(self.basedir, "basic-des-cbc.crypt")]) + self.set_options() + Bcfg2.Options.setup.algorithm = "des_cbc" + cli.run() + self.assertExists("basic-des-cbc") + actual = open(os.path.join(self.basedir, "basic-des-cbc")).read() + self.assertEqual(self.cfg_plaintext, actual) + self.assertNotEncrypted(actual) + + def test_cfg_auto_passphrase(self): + """ Discover the passphrase to decrypt a Cfg file""" + self._cfg_decrypt(["--decrypt", "--cfg"], "complex.crypt") + + def test_cfg_auto_mode(self): + """ Discover whether to encrypt or decrypt a Cfg file """ + self._cfg_decrypt(["--cfg", "-p", "basic"], "basic.crypt") + self._cfg_encrypt(["--cfg", "-p", "basic"], "plaintext") + + def test_cfg_auto_type(self): + """ Discover a file is a Cfg file """ + self._cfg_decrypt(["--decrypt", "-p", "basic"], "basic.crypt") + self._cfg_encrypt(["--encrypt", "-p", "basic"], "plaintext") + + def test_cfg_multiple(self): + """ Decrypt multiple Cfg files """ + cli = CLI(["--decrypt", "--cfg", "-p", "basic", + os.path.join(self.basedir, "basic.crypt"), + os.path.join(self.basedir, "basic2.crypt")]) + self.set_options() + cli.run() + self.assertExists("basic") + self.assertExists("basic2") + actual1 = open(os.path.join(self.basedir, "basic")).read() + actual2 = open(os.path.join(self.basedir, "basic2")).read() + self.assertEqual(self.cfg_plaintext, actual1) + self.assertEqual(self.cfg_plaintext, actual2) + self.assertNotEncrypted(actual1) + self.assertNotEncrypted(actual2) + + def test_cfg_auto_all(self): + """ Discover all options to encrypt/decrypt Cfg files """ + self._cfg_decrypt([], "complex.crypt") + self._cfg_encrypt(["-p", "basic"], "plaintext") + + def test_cfg_stdout(self): + """ Decrypt a Cfg file to stdout """ + cli = CLI(["--decrypt", "--cfg", "-p", "basic", "--stdout", + os.path.join(self.basedir, "basic.crypt")]) + self.set_options() + old_stdout = sys.stdout + sys.stdout = StringIO() + cli.run() + output = sys.stdout.getvalue() + sys.stdout = old_stdout + + self.assertNotExists("basic") + self.assertEqual(self.cfg_plaintext.strip(), output.strip()) + self.assertNotEncrypted(output) + + def test_encrypt_cfg(self): + """ Encrypt a Cfg file """ + self._cfg_encrypt(["--encrypt", "--cfg", "-p", "basic"], "plaintext") + os.rename(os.path.join(self.basedir, "plaintext.crypt"), + os.path.join(self.basedir, "test.crypt")) + self._cfg_decrypt(["--decrypt", "--cfg", "-p", "basic"], + "test.crypt") + + def test_encrypt_props_as_cfg(self): + """ Encrypt an XML file as a Cfg file """ + cli = CLI(["--encrypt", "--cfg", "-p", "basic", + os.path.join(self.basedir, "plaintext.xml")]) + self._encrypt(cli, "plaintext.xml.crypt", "plaintext.xml") + + os.rename(os.path.join(self.basedir, "plaintext.xml.crypt"), + os.path.join(self.basedir, "test.xml.crypt")) + cli = CLI(["--decrypt", "--cfg", "-p", "basic", + os.path.join(self.basedir, "test.xml.crypt")]) + self._decrypt(cli, "test.xml", "plaintext.xml") + + def test_cfg_remove(self): + """ Encrypt and remove a Cfg file """ + test = os.path.join(self.basedir, "test") + shutil.copy(os.path.join(self.basedir, "plaintext"), test) + self._cfg_encrypt(["--encrypt", "--remove", "--cfg", "-p", "basic"], + test) + self.assertNotExists("test") + + def test_decrypt_props(self): + """ Decrypt a Properties file """ + self._props_decrypt(["--decrypt", "--properties", "-p", "basic"], + "all-basic.xml", "plaintext2.xml") + + def test_props_decrypt_multiple_passphrases(self): + """ Decrypt a Properties file with multiple passphrases""" + self._props_decrypt(["--decrypt", "--properties"], + "plaintext-all.xml", "plaintext.xml") + + def test_props_decrypt_mixed(self): + """ Decrypt a Properties file with mixed encrypted content""" + self._props_decrypt(["--decrypt", "--properties"], + "plaintext-xpath.xml", "plaintext.xml") + + def test_props_decrypt_bogus(self): + """ Decrypt a malformed Properties file """ + self._props_decrypt(["--decrypt", "--properties"], + "bogus-forced.xml", "bogus.xml") + + def test_props_decrypt_auto_type(self): + """ Discover an encrypted file is a Properties file """ + self._props_decrypt(["--decrypt"], + "all-basic.xml", "plaintext2.xml") + + def test_props_decrypt_auto_mode(self): + """ Discover whether to encrypt or decrypt an encrypted Properties file """ + self._props_decrypt(["--properties"], + "all-basic.xml", "plaintext2.xml") + + def test_props_decrypt_auto_all(self): + """ Discover all options to decrypt a Properties file """ + self._props_decrypt([], "all-basic.xml", "plaintext2.xml") + + def test_props_encrypt_cli_passphrase(self): + """ Encrypt a Properties file with passphrase on the CLI""" + self._props_encrypt(["--encrypt", "--properties", "-p", "basic"], + "plaintext2.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--decrypt", "--properties", "-p", "basic"], + "encrypted.xml", "plaintext2.xml") + + def test_props_encrypt_file_passphrase(self): + """ Encrypt a Properties file with passphrase in the file """ + self._props_encrypt(["--encrypt", "--properties"], "plaintext2.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--decrypt", "--properties"], + "encrypted.xml", "plaintext2.xml") + + def test_props_encrypt_multiple_passphrases(self): + """ Encrypt a Properties file with multiple passphrases """ + self._props_encrypt(["--encrypt", "--properties"], "plaintext.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--decrypt", "--properties"], + "encrypted.xml", "plaintext.xml") + + def test_props_encrypt_xpath(self): + """ Encrypt a Properties file with --xpath """ + test = os.path.join(self.basedir, "test.xml") + self._props_encrypt(["--encrypt", "--properties", "--xpath", "//Foo"], + "plaintext.xml", check_all=False) + xdata = lxml.etree.parse(test) + for el in xdata.iter(): + if el.tag is not lxml.etree.Comment and el.text.strip(): + if el.tag == "Foo": + self.assertIsEncrypted(el.text) + else: + self.assertNotEncrypted(el.text) + + os.rename(test, os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--decrypt", "--properties"], + "encrypted.xml", "plaintext.xml") + + def test_props_encrypt_bogus(self): + """ Decrypt a malformed Properties file """ + self._props_encrypt(["--encrypt", "--properties"], "bogus.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--decrypt", "--properties"], + "encrypted.xml", "bogus.xml") + + def test_props_encrypt_auto_type(self): + """ Discover if a file is a Properties file """ + self._props_encrypt(["--encrypt"], "plaintext2.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--decrypt"], + "encrypted.xml", "plaintext2.xml") + + def test_props_encrypt_auto_mode(self): + """ Discover whether to encrypt or decrypt a Properties file """ + self._props_encrypt(["--properties"], "plaintext2.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt(["--properties"], + "encrypted.xml", "plaintext2.xml") + + def test_props_encrypt_auto_all(self): + """ Discover all options to encrypt a Properties file """ + self._props_encrypt([], "plaintext.xml") + os.rename(os.path.join(self.basedir, "test.xml"), + os.path.join(self.basedir, "encrypted.xml")) + self._props_decrypt([], "encrypted.xml", "plaintext.xml") -- cgit v1.2.3-1-g7c22