From 2a1b25487c169f8fa653be5ffca10ad4ec49345a Mon Sep 17 00:00:00 2001 From: Rowen S Date: Thu, 28 Nov 2024 13:38:54 -0500 Subject: [PATCH 1/3] Validate that a private key file is supplied during config --- files/validate_config.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/files/validate_config.py b/files/validate_config.py index bf87f455..dfcb7845 100755 --- a/files/validate_config.py +++ b/files/validate_config.py @@ -83,6 +83,20 @@ def confirm_submission_privkey_file(self): """ if not os.path.exists(self.secret_key_filepath): raise ValidationError(f"PGP secret key file not found: {self.secret_key_filepath}") + with open(self.secret_key_filepath) as f: + for line in f: + sline = line.strip() + if not sline: + # Whitespace at top of file + continue + if sline.startswith("-----BEGIN PGP PRIVATE KEY BLOCK-----"): + # Good enough; it is imported later to check it's well-formed + break + else: + # Expecting a file with an armored secret key only + raise ValidationError( + "PGP secret key file provided is not an armored private key" + ) gpg_cmd = ["gpg", "--import", self.secret_key_filepath] result = False with tempfile.TemporaryDirectory() as d: From 001d419020268569f9a2fba4d8e9c868d1ca2e9e Mon Sep 17 00:00:00 2001 From: Rowen S Date: Thu, 28 Nov 2024 13:39:27 -0500 Subject: [PATCH 2/3] Add SDWConfigValidator unit tests and sample config files --- tests/files/example_key.asc | 106 +++++++++++++++++++++ tests/files/example_key.asc.malformed | 3 + tests/files/testconfig.json | 13 +++ tests/files/testconfig.json.malformedfpr | 12 +++ tests/files/testconfig.json.malformedonion | 13 +++ tests/test_dom0_validate.py | 70 ++++++++++++++ 6 files changed, 217 insertions(+) create mode 100644 tests/files/example_key.asc create mode 100644 tests/files/example_key.asc.malformed create mode 100644 tests/files/testconfig.json create mode 100644 tests/files/testconfig.json.malformedfpr create mode 100644 tests/files/testconfig.json.malformedonion create mode 100644 tests/test_dom0_validate.py diff --git a/tests/files/example_key.asc b/tests/files/example_key.asc new file mode 100644 index 00000000..edf126d7 --- /dev/null +++ b/tests/files/example_key.asc @@ -0,0 +1,106 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v2.0.19 (GNU/Linux) + +lQcYBFJZi2ABEACZJJA53+pEAdkZyD99nxB995ZVTBw60SQ/6E/gws4kInv+YS7t +wSMXGa5bR4SD9voWxzLgyulqbM93jUFKn5GcsSh2O/lxAvEDKsPmXCRP1eBg3pjU ++8DRLm0TEFiywC+w6HF4PsOh+JlBWafUfL3vwrGKTXvrlKBsosvDmoogLjkMWomM +KBF/97OKyQiMQf1BDJqZ88nScJEqwo0xz0PfcB04GAtfR7N6Qa8HpFc0VDQcILFB +0aJx5+p7nw1LyR37LLoK8JbEY6QZd277Y0/U+O4v6WfH/2H5kQ8sC+P8hPwr3rSg +u3SVbNRasB4ZHFpJZR9Kv21zmQb9U3rrCk2yg3Wm0qtZ0S5CECAAwG2LQkKouRw2 +ak+Y8aolHDt6a785eF0AaAtgbPX4THMum/CNMksHO0PBBqxR+C9z7WSHXFHvv+8B +5nRccS4m4klyYTbZOOJ45DuC3xDjTRwzzpkYhqf4pLAhwF3spKZsAczAFPmDyxFf +CyIBiMZSK/j8PMJT1X5tgpL1NXImNdVIPV2Fy+W7PkNfG2FL/FQIUnK6ntukLW/7 +hV6VHcx52mMn1pVUc6v80LEb4BMDz41vlj9R8YVv8hycPtnN0QL5gIME1n7jbKJf +yfWxkvBXMINDgHK/RysRMP6FXA6Mw65BGNIuO0Il0FTy12HuKI/coEsG2QARAQAB +AA//Q5Azhy0IDDfqgarsg+4U1xZPv1MEU1iozv8dmpInYx7JqHlUvHUMl6jvWPsM +9jGUtU7t3en3n8ngoCR0LUmH8uLf8IXWL2s2TIjmA7AcHxLDWslqEPD+6Oq8GYCJ +OVd70udCBGRgaAmnB4NX/XGJVImHTXaQ2Obp/fO2xRXdoYPzDEW3UFvvGI9+KRk3 +SbXlVvkKDijVnh+mlABgTZzdG2s5oOFOxxr5jlMDNvJkvMP3d39e5KRpsCo6s46A +zbItpX5el+v8ACnboJamIod2lYW7g+zMKhq8LWA3mt2mGGbNYEdxVkZNkY0BhP8V +UEvHc4EHFLGuxqS5RjM51A9oJk6CES2rs8Q68rXuUKpIoolq4KCNSQvetOGLPiks +EICbJcC+3pwg1OhOCbD2nV8kHHSiuEbQCt4UBNzw+g4ponW9IwadKz1WSGpdRlzi +Ksn+jpAzIi8b50tEIFqCMEF/zH+V1dU3TtVmKpI4KshBtmvkWt4Ea460Ve8q5Oku +4AG7Iujiz/KAtWYU9AnzzalyB4Zy0yGqeNZ0faxnewtVSpqhJ+Qcxv6IuOcNYZow +1ese5ncRh3OPwskyRhl+9B9YOEVky+vUFa2IB5K/0CnFC86MMjlJ97uRJJ+4ompV +rWCSpNifBgjPc+7q1jLqJMkE5pc45ZCEIvR9SvHOjI/uSU8IAMFtM8WW6LXmb7z0 +intLj4rPSgnic5PtQP/XghiqNeMLVSRfTo+xO0IqMIRFEeCjDiQ74nh4k6WDdQpG +Uq3+5SeV1VJSRLpjBUZBEdX0XBhzS5XvKVzCnXSVl7JzL9mGHk1QWziLLimlu49R +m3qt5g30UkX56A6aJ6VpJc4P5wwV9Mxnjp4B/D34xGEfX7YaNYE859/y9NhXlHuV +dd0esfYnTV4UPifBJvopeRy0P/RICkozE9sgRgg1RVfDWEyLcljCQNgxrra3sMLY +jlK3wvAEdXf1Gb1024Knbp5u8gTZgqh/PREDXI2eqdCSuLdygcJAsGJHkdZtYUSK +epWGGicIAMqvOd6wvfEvz2Comn/t8gwuAv49TUOMGMTmpR4VSuKePZ8f+olUqy4X +Fo0wCzq+K+DYPH+JL9S9nXW29E20EM6Khd+lREMNcUf/G2Cb3mjfz27GyhRiACYq +Nrvsn0pHstXTJqnQyznZlbgGmk+gzfsK9aMT3W9XZFjODDsHEvHYF0zcO212AjCj +COJuZePP44eDqiu9Owxv15KwqtgHlaVz5kg9j1cA58ppmd/lRvep7aR3tuuKiXyb +htunNaitKTwB475oO+W/x7RsL9oZh85i8R+YSzyqabEg7VNTazk82boo2sDsuaiu +ZQspK6juGR50vDWiAJmuGYWzEGmvdv8IAJLYwi82TLg9OcDwaoBl295b/Pc5ar21 +LRSDPf//qAsXrN8YkrOm7BsfRp9tMzgCEpkCgDj3JZDLh1TlmX8Gmsa/xVq+bfNP +8W0ELulOrcCQ0aAQxrJRCHjnUAzcI2tjzT6961PrrEYTsy7tlZ7mYZ2SmPyrPZEh +SNVnO8H3rDaBXaqqLOi+SzrSkYn9DjA+IEp4Pi1J8mZWs5vV662xrqnHPhzNKf6Y +dAAF5GlXOrEqCj2qF/i79P9kh5KHr37ZsgFl11zesVEyezL2sScv6KmeRjz3O3Nk +TagLhJTzBNoUZymiq5CQlY2nn5c5UeFx9lpRHnJRkv9p8adspqwYKguBi7Q2U2Vj +dXJlRHJvcCBUZXN0L0RldmVsb3BtZW50IChETyBOT1QgVVNFIElOIFBST0RVQ1RJ +T04piQI4BBMBAgAiBQJSm8UDAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK +CRDMQO8SKCcUQReED/4uGk1OGSJHip2EsgAPrwL6L3aT9FMKt+eQCLoj5DdoH3tY +0mXGMP/0M/oIq2Y+q6BEXVNEYOy2QzTnnPqn965tqN/SZF1CNu/IYmxCJj7TSJi/ +MuWtg7IebR8KvWLKJjW4PU5ybmB2hzyO3jTEzXY3j8bocGfx3Q8B6ot/MdK8ss5J +rLSIPlgQHhyXloe4CTTk0alQbtt8KEp0kMXmqjrz66AsofwjzcezOn1PSc0S4tV7 +0OkIEapevBcr7cnYQv3gWSXpK4zZNg9NZ5dLR73g64Lv+GqK0UBksueMfEEmx/uD +Bd7/uxmz7jWFb3D9MBLCjAMQ+s8Kh8bJQ/HPMjIh8T9y8ek/dI5Il7ehFaci1yzT ++qIPt7SArj3q4KR5lCNeIK7Bu8Kuu2VgfCRske2PJAQlauu7jO3XZcLSuihwTdLL +se+WIdW6miyczJNAt1pHknHsdXANegJh7eoAy+ghFok7kZpYMTR/iy95EqNxAt6l +LivYeyzUtfPjHjDpqPUtrZRGipqmFwIcTn5E/HokkViUSizx0Sd2LyJ2tox6a+az +lreW4hRY5WVPclVeTynvAtMrSl1DEErRoVK/AZKnBgUEeDCd9g/EiRzOLrX5azb9 +nCiFlLMeWWVmbefvnCXrXJqVQNrVZdelSZakJpJ/oQpKyv/5nU9pcgeVrzP68Z0H +GARSWYtgARAA837/vToG+ChFhaJvczBfsYPG3Hfwre9v7Fi0Fuj8+vkpJixB7pJU +zvpO8YkOo3c1849a038t9ey+xudZ2gUm+hJH7/JrtqIDsK77YGJxgr3wqaKFEsXH +4vmhCcyCS9vUwItUQi2ZteSkW5LxJfMEvdwUi4moOcOP/Hj9b13m6veRqwmcIjWX +YXULN6p+I91Ub01v0mRyAHSWPpjH1DD46uHOLAPNqLOpFaxJ1nixn0/XfpJ35vSf +9kbpsvdGywGOkhZkWffw8cCsGyLFcvAkb1N0VRUl/BwgUHqUQkJJbPa+ylQamBNl +oftGvvcBxxzSO1QcShlz35a4q8WNQAeb4y3F9YZl2wqMn+MrYHR8gig8/TnPsZIC +XslVul8EqnORIbjRV6d/guwRe3kGdURCS2y+grRJHhdIxwWk3ijP6TeH1YYz4lPx +bDZmRiscS8sQ55wyOaWPG4aYVccAUWeRrVTaolTQ8Pq0QAkGpaU9tTnAICz/kc/q +n90z8hGTeljxMfP++iC7kh2/JqTh+1v+deH+TbhWgJYJlJzt3E9dIYeCMDkPpL49 +KjMMPHwEiPQyRMV1GG98Q0gpjpCT4btfw6694HRQYWuP4wM+4wVpbFa9kSyc4pX/ +DIvY/FqRyHz/ll7cFs9/omD0tEj6Ae4PQwNPhIu+tKSSX+9wBIw/nxcAEQEAAQAP +/1bzlAmTridx4hmtftUIgjOW1i2mmwjRxwsERhMkUiqhTSN3jHfQQ37B/ezcv6B6 +EocOOyXpdZUrXJkUxo5HZrrISm4SCIroYh727YdmwBgrEcTR52ljvVR9RheEs0a5 +ksjLOGSFei1tH5Af8gNWO+w8qg2GM8+k2UcUQZRCWRKxI5CLVvkUYCGKNV5EgNT3 +1Y4FfhgIjHlDKN/jmQBaGJlv1zr6hLdoqMm3g4qWAP/d+BsX3L9ZvcGpYwzoppwZ +yzq5yk4ibyU1Y4AxM4cu4CPtDk7PxYe414VFsKnUl/nURx9jVzfVPWbRn1rURAtB +bIWJLKz9V9aRMRMN8bnavbx5HrtGXanVzsGz1ZXlpnGAWeG2E2GFM42VQ1206gLn +15sB1ZIrzLSDoCRa4eL7agt0zOyJ7PNBT1qZDvmulva+amdvzPwBHIaIALSQVPap +17sO+bV6FN7dnHgKta1hWKdbeFJpoN0+TmIHAad/LO+qLeO0bA4/WgTXTN7uAiNG +Tapp0x79xHVjC8JUF9tmArNVYQuybwBbZ2z3dYaYa+7dvdSGS9zUMWNwdGH2BnzQ +LRGMyfQJAMXaivNdwHluuMuYyhBFstFhgH/4vYXLeJ2p0vdtFf8QqeFaEirHzBQX +X8DJmfWySb1XcPsC7RUjgI+6rPNJZ53vjHQEr22QPFUhCADzoVVzPpLLIXJ+/Mee +DA4vRg476cfCY+EW/cOu7kL+VzJZmgd7t06ZHU+TL1yDFkxajaJBQMz2RT5kLWtv +FSf9cGPfdv7L6J9y7UiojTzdIFnJH5VDmo5ozntvJrUcmG5/vI2eyc9PAIX8Q+8v +iKo2zFqs6+x+8gOES/3hZWHHC8rA2JsdJBk796vuWxgDIB67M2mA7L5qjyinkKrY +cthDBNJ3PqfToFuvENS835hxluwyNQcaS1UTr39KD0qsXqvmmSZf/LVDBIJ89uXU +pSY7hA0HSeWCA2haIxVzrzqPBlmZEagdqcfP9bsf4VsmuDZwYQkd9sgzWYqX2zed +vXORCAD/2+wCvCrnoOn1U0yt6xjKCHe84IZh1jn0cnf1inSboHGjDU7otPWskWUF +EVjdCFks5jR7jGaLUi85QfMQW4Sqbl1x9vFmk+xLVFxvrDibuDWg4JpuPyStvTCJ +6K7jda1bQI+p0TGg5g1o8fDaUTex9J1zNyJ+vzlN3zuvcOKPdRkPDppsd7noTBBm +lZhoNus3w+7/MO8RrRBskcDfUefwHILvxBFh3VapQ2ke17l4UJJSkFabxSnOj/th +j3B26L1d0oV3bly6faTKb22puR1l+/jRcOpX+pzroZGDpmdBjvRdctepDsWxeDxK +82Sw8NLkJ8pviD7MZ0BVK3q1aMQnB/4t7Ri0c+I1brdBtChELhYiXmU1+LMXs2GM +dchHxJWpt0RhexHvIP1/mBwePr0uI2QVnA+UpZ/lAj14KxWje7K08FoRSLVsxZnx +6ArKiqROJEIF1xpAYf2OK9TffFVCvFCu9EQqx61TLgNhbXreAELM0e2dcf3iocFq +VA+dgmk6X5HdRPujta9gQ1STrw/s6wQ4aRv+ionItuLv8zUpULxTK0gOAPTNEMCR +HO31+RmR1nse8LGtgTotVqSRa6cmFBUCi8OJJSAY9233fZXwJl0FEFk2S52zrTCY +QVz0jqDU4hQ+zZfI82Z9yOMFAK8wcVk+YbKV+agfHf5PfaDLz56Fg/CJAh8EGAEC +AAkFAlJZi2ACGwwACgkQzEDvEignFEGn9w//eUnH3PnLNkDpS8tBHqkr5XWLLaG9 +n5L4TBhEKJOBhNd6QfMtdbCNYZ9RgNMcx5pL070ExEwY5TeKfJvjsZlKhDQ3RtFV +POtjr/SJ+FRInTQx6Y6h0jVvPikAyTe5HyJbKGVoafskAgAqYKb4rSqR4l3rVL2L +KvHuz1CZo0+e6mbmlz5uk4CRsrKruwQWlYzlDHzafW1Uy2chbY6hE9vPzQmSRAHa +mXpKOyRepnz2NwVYYjogKFgQ0pzrnFp8O3i4W4dT7mPiPZ/jJJhLB+hYL3sw6Aku +oD9aKbF540JgWHKRQNasvmYoFOAxeAf+xiTcYOjt+yxphsqfXFttfgZdCXf6u7jN +Pr8XsLFkSuMtv569KHJ/iK0z7kB1spGJHOitqopuUFrhN8kFKoeKx1zF1l4F7X36 +PJjprxkxwaGtB6SyIrFNGHvKUCTsItWAsQgcvFfMehnSgAXPa6Ub7Mf0pL097wxD +EcKuXJ+hASVC4mhhutgE67byK28Y+DPr7nGC9lE68+ioiQiTwNi32UmpQUF5m4Ul +3lbVO4covG55Vi9Ip4b57dOM5h0kW8Nkiczhw1avw33aZhKKmGWOIcApVNB4h/WZ +rTtBQf+6XdgL6DTsX4EuicghcDq5BV5u/mIvFOA7MhDAdMlW7gw+JA2fWHh2TVGi +d9X9on517X6qMDw= +=E6hg +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/files/example_key.asc.malformed b/tests/files/example_key.asc.malformed new file mode 100644 index 00000000..918020a6 --- /dev/null +++ b/tests/files/example_key.asc.malformed @@ -0,0 +1,3 @@ +Version: GnuPG v2.0.19 (GNU/Linux) + +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/files/testconfig.json b/tests/files/testconfig.json new file mode 100644 index 00000000..5c9e58f6 --- /dev/null +++ b/tests/files/testconfig.json @@ -0,0 +1,13 @@ +{ + "submission_key_fpr": "65A1B5FF195B56353CC63DFFCC40EF1228271441", + "hidserv": { + "hostname": "sdolvtfhatvsysc6l34d65ymdwxcujausv7k5jk4cy5ttzhjoi6fzvyd.onion", + "key": "5U4JPYSZ34N2ZDSOUAL2YLEX2NPI5BLL2Y66QJW24KLSH7R3FEPQ" + }, + "environment": "prod", + "vmsizes": { + "sd_app": 10, + "sd_log": 5 + } + } + \ No newline at end of file diff --git a/tests/files/testconfig.json.malformedfpr b/tests/files/testconfig.json.malformedfpr new file mode 100644 index 00000000..762e4283 --- /dev/null +++ b/tests/files/testconfig.json.malformedfpr @@ -0,0 +1,12 @@ +{ + "submission_key_fpr": "65A1B5FF195B56353CC63DFFCC40EF1228271", + "hidserv": { + "hostname": "sdolvtfhatvsysc6l34d65ymdwxcujausv7k5jk4cy5ttzhjoi6fzvyd.onion", + "key": "5U4JPYSZ34N2ZDSOUAL2YLEX2NPI5BLL2Y66QJW24KLSH7R3FEPQ" + }, + "environment": "prod", + "vmsizes": { + "sd_app": 10, + "sd_log": 5 + } +} diff --git a/tests/files/testconfig.json.malformedonion b/tests/files/testconfig.json.malformedonion new file mode 100644 index 00000000..60fd1c54 --- /dev/null +++ b/tests/files/testconfig.json.malformedonion @@ -0,0 +1,13 @@ +{ + "submission_key_fpr": "65A1B5FF195B56353CC63DFFCC40EF1228271441", + "hidserv": { + "hostname": "sdolvtfhatvsysc6l34d65ymdwxcuj.onion", + "key": "5U4JPYSZ34N2ZDSOUAL2YLEX2NPI5BLL2Y66QJW24KLSH7R3FEPQ" + }, + "environment": "prod", + "vmsizes": { + "sd_app": 10, + "sd_log": 5 + } + } + \ No newline at end of file diff --git a/tests/test_dom0_validate.py b/tests/test_dom0_validate.py new file mode 100644 index 00000000..f2f4ada7 --- /dev/null +++ b/tests/test_dom0_validate.py @@ -0,0 +1,70 @@ +import shutil +import tempfile +import unittest +from pathlib import Path + +from files.validate_config import SDWConfigValidator, ValidationError + + +class SD_Dom0_Validate_Tests(unittest.TestCase): + def setUp(self): + # Enable full diff output in test report, to aid in debugging + self.maxDiff = None + self.test_resources = Path(__file__).parent.resolve() + + def test_good_config(self): + with tempfile.TemporaryDirectory() as dir: + shutil.copy(f"{self.test_resources}/files/testconfig.json", f"{dir}/config.json") + shutil.copy(f"{self.test_resources}/files/example_key.asc", f"{dir}/sd-journalist.sec") + + # Validator currently runs checks in constructor + SDWConfigValidator(dir) + + def test_missing_config(self): + with tempfile.TemporaryDirectory() as dir: + with self.assertRaises(ValidationError) as err: # noqa: PT027 + SDWConfigValidator(dir) + + assert "Config file does not exist" in str(err.exception) + + def test_config_malformed_key(self): + with tempfile.TemporaryDirectory() as dir: + shutil.copy(f"{self.test_resources}/files/testconfig.json", f"{dir}/config.json") + shutil.copy( + f"{self.test_resources}/files/example_key.asc.malformed", f"{dir}/sd-journalist.sec" + ) + + with self.assertRaises(ValidationError) as err: # noqa: PT027 + SDWConfigValidator(dir) + + assert "PGP secret key file provided is not an armored private key" in str( + err.exception + ) + + def test_config_malformed_onion_json(self): + with tempfile.TemporaryDirectory() as dir: + shutil.copy( + f"{self.test_resources}/files/testconfig.json.malformedonion", f"{dir}/config.json" + ) + shutil.copy(f"{self.test_resources}/files/example_key.asc", f"{dir}/sd-journalist.sec") + + with self.assertRaises(ValidationError) as err: # noqa: PT027 + SDWConfigValidator(dir) + + assert "Invalid hidden service hostname specified" in str(err.exception) + + def test_config_malformed_fpr_json(self): + with tempfile.TemporaryDirectory() as dir: + shutil.copy( + f"{self.test_resources}/files/testconfig.json.malformedfpr", f"{dir}/config.json" + ) + shutil.copy(f"{self.test_resources}/files/example_key.asc", f"{dir}/sd-journalist.sec") + + with self.assertRaises(ValidationError) as err: # noqa: PT027 + SDWConfigValidator(dir) + + assert "Invalid PGP key fingerprint specified" in str(err.exception) + + +def load_tests(loader, tests, pattern): + return unittest.TestLoader().loadTestsFromTestCase(SD_Dom0_Validate_Tests) From f23af0689473dae66c2b6462f44976bddb4a5ff3 Mon Sep 17 00:00:00 2001 From: Rowen S Date: Fri, 6 Dec 2024 15:54:28 -0500 Subject: [PATCH 3/3] Include password-protected keyfile check in SDWConfigValidator. --- files/validate_config.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/files/validate_config.py b/files/validate_config.py index dfcb7845..46318be3 100755 --- a/files/validate_config.py +++ b/files/validate_config.py @@ -103,14 +103,12 @@ def confirm_submission_privkey_file(self): gpg_env = {"GNUPGHOME": d} # Call out to gpg to confirm it's a valid keyfile try: - subprocess.check_call( - gpg_cmd, env=gpg_env, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL - ) + subprocess.check_output(gpg_cmd, env=gpg_env, stderr=subprocess.STDOUT) result = True - except subprocess.CalledProcessError: - # suppress error since "result" is checked next - pass - + except subprocess.CalledProcessError as err: + if err.output and "No pinentry" in err.output.decode(): + raise ValidationError("PGP key is passphrase-protected.") + # Otherwise, continue; "result" is checked next if not result: raise ValidationError(f"PGP secret key is not valid: {self.secret_key_filepath}")