diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b758f36 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "customizations": { + "vscode": { + "extensions": [ + "scheiblingco.code-pypack", + "scheiblingco.code-cmd-repeat" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/deploy_testing.yml b/.github/workflows/deploy_testing.yml index 3590d24..79ce510 100644 --- a/.github/workflows/deploy_testing.yml +++ b/.github/workflows/deploy_testing.yml @@ -1,3 +1,4 @@ +name: "Release Tests" on: release: types: [created] diff --git a/.github/workflows/dev_testing.yml b/.github/workflows/dev_testing.yml index 7c84d47..ead4678 100644 --- a/.github/workflows/dev_testing.yml +++ b/.github/workflows/dev_testing.yml @@ -1,3 +1,4 @@ +name: "Development Tests" on: push: paths-ignore: diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 5a22f12..8f1f6cb 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,3 +1,4 @@ +name: "Pylint" on: push: paths-ignore: @@ -23,12 +24,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pylint pylint-report pylint-json2html + pip install -r requirements-test.txt - name: Run tests run: | - pylint src/sshkey_tools | tee | pylint-json2html -o report.html + python3 -m pylint src/sshkey_tools | tee | pylint_report -o report.html - name: Upload report uses: actions/upload-artifact@v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2677b16..29b311d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,3 +1,4 @@ +name: "Build Release" on: release: types: [published] diff --git a/.gitignore b/.gitignore index ef8570c..17a3ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -148,4 +148,13 @@ report.html test_certificate testing.py .idea -core \ No newline at end of file +core +/id_rsa +/id_rsa.pub +/id_ecdsa* +/id_dsa* +/id_ed25519* +hello.txt +hello.txt.sig + +/testkeys \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 7321f5e..c6120ab 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,6 @@ +[MASTER] +load-plugins=pylint_report +extension-pkg-allow-list=cryptography.hazmat.bindings._rust + [REPORTS] -output-format=json \ No newline at end of file +output-format=pylint_report.CustomJsonReporter \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..92fb067 --- /dev/null +++ b/Pipfile @@ -0,0 +1,25 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +click = ">=7.1" +cryptography = ">=41.0" +bcrypt = ">=4.0" +enum34 = ">=1.1" +prettytable = ">=3.1" +pytimeparse2 = ">=1.4" + +[dev-packages] +pylint-report = "*" +paramiko = "*" +coverage = "*" +black = "*" +pytest-cov = "*" +faker = "*" +cprint = "*" +pylint = "*" + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..10bd3b0 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,768 @@ +{ + "_meta": { + "hash": { + "sha256": "3e9ffc836ed7f6927d399ab3e7be4c355934ccac3df59207aad55166be37cedd" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "bcrypt": { + "hashes": [ + "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", + "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", + "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", + "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", + "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", + "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", + "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", + "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", + "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", + "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", + "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", + "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", + "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", + "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", + "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", + "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", + "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", + "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", + "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", + "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", + "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "python_version >= '3.8'", + "version": "==1.16.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "cryptography": { + "hashes": [ + "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", + "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", + "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", + "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", + "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", + "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", + "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", + "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", + "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", + "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", + "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", + "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", + "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", + "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", + "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", + "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", + "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", + "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", + "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", + "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", + "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", + "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", + "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==41.0.4" + }, + "enum34": { + "hashes": [ + "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53", + "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328", + "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248" + ], + "index": "pypi", + "version": "==1.1.10" + }, + "prettytable": { + "hashes": [ + "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8", + "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.9.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pytimeparse2": { + "hashes": [ + "sha256:98668cdcba4890e1789e432e8ea0059ccf72402f13f5d52be15bdfaeb3a8b253", + "sha256:a162ea6a7707fd0bb82dd99556efb783935f51885c8bdced0fce3fffe85ab002" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==1.7.1" + }, + "wcwidth": { + "hashes": [ + "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704", + "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4" + ], + "version": "==0.2.8" + } + }, + "develop": { + "argcomplete": { + "hashes": [ + "sha256:d5d1e5efd41435260b8f85673b74ea2e883affcbec9f4230c582689e8e78251b", + "sha256:d97c036d12a752d1079f190bc1521c545b941fda89ad85d15afa909b4d1b9a99" + ], + "markers": "python_version >= '3.6'", + "version": "==3.1.2" + }, + "astroid": { + "hashes": [ + "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca", + "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.1" + }, + "bcrypt": { + "hashes": [ + "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", + "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", + "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", + "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", + "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", + "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", + "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", + "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", + "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", + "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", + "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", + "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", + "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", + "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", + "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", + "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", + "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", + "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", + "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", + "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", + "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "black": { + "hashes": [ + "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884", + "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916", + "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258", + "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1", + "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce", + "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d", + "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982", + "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7", + "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173", + "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9", + "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb", + "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad", + "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc", + "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0", + "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a", + "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe", + "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace", + "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==23.10.1" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "python_version >= '3.8'", + "version": "==1.16.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "coverage": { + "hashes": [ + "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1", + "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63", + "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9", + "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312", + "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3", + "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb", + "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25", + "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92", + "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda", + "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148", + "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6", + "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216", + "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a", + "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640", + "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836", + "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c", + "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f", + "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2", + "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901", + "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed", + "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a", + "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074", + "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc", + "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84", + "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083", + "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f", + "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c", + "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c", + "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637", + "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2", + "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82", + "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f", + "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce", + "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef", + "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f", + "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611", + "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c", + "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76", + "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9", + "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce", + "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9", + "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf", + "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf", + "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9", + "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6", + "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2", + "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a", + "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a", + "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf", + "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738", + "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a", + "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==7.3.2" + }, + "cprint": { + "hashes": [ + "sha256:83469274db24939b27a9b3a03d5298fec6f07408c3175dc0fc21d5ba206f51c8" + ], + "index": "pypi", + "version": "==1.2.2" + }, + "cryptography": { + "hashes": [ + "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", + "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", + "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", + "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", + "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", + "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", + "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", + "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", + "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", + "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", + "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", + "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", + "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", + "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", + "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", + "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", + "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", + "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", + "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", + "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", + "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", + "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", + "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==41.0.4" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version < '3.11'", + "version": "==0.3.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" + ], + "markers": "python_version < '3.11'", + "version": "==1.1.3" + }, + "faker": { + "hashes": [ + "sha256:14ccb0aec342d33aa3889a864a56e5b3c2d56bce1b89f9189f4fbc128b9afc1e", + "sha256:da880a76322db7a879c848a0771e129338e0a680a9f695fd9a3e7a6ac82b45e1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==19.13.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.12.0" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "markupsafe": { + "hashes": [ + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.3" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "numpy": { + "hashes": [ + "sha256:06934e1a22c54636a059215d6da99e23286424f316fddd979f5071093b648668", + "sha256:1c59c046c31a43310ad0199d6299e59f57a289e22f0f36951ced1c9eac3665b9", + "sha256:1d1bd82d539607951cac963388534da3b7ea0e18b149a53cf883d8f699178c0f", + "sha256:1e11668d6f756ca5ef534b5be8653d16c5352cbb210a5c2a79ff288e937010d5", + "sha256:3649d566e2fc067597125428db15d60eb42a4e0897fc48d28cb75dc2e0454e53", + "sha256:59227c981d43425ca5e5c01094d59eb14e8772ce6975d4b2fc1e106a833d5ae2", + "sha256:6081aed64714a18c72b168a9276095ef9155dd7888b9e74b5987808f0dd0a974", + "sha256:6965888d65d2848e8768824ca8288db0a81263c1efccec881cb35a0d805fcd2f", + "sha256:76ff661a867d9272cd2a99eed002470f46dbe0943a5ffd140f49be84f68ffc42", + "sha256:78ca54b2f9daffa5f323f34cdf21e1d9779a54073f0018a3094ab907938331a2", + "sha256:82e871307a6331b5f09efda3c22e03c095d957f04bf6bc1804f30048d0e5e7af", + "sha256:8ab9163ca8aeb7fd32fe93866490654d2f7dda4e61bc6297bf72ce07fdc02f67", + "sha256:9696aa2e35cc41e398a6d42d147cf326f8f9d81befcb399bc1ed7ffea339b64e", + "sha256:97e5d6a9f0702c2863aaabf19f0d1b6c2628fbe476438ce0b5ce06e83085064c", + "sha256:9f42284ebf91bdf32fafac29d29d4c07e5e9d1af862ea73686581773ef9e73a7", + "sha256:a03fb25610ef560a6201ff06df4f8105292ba56e7cdd196ea350d123fc32e24e", + "sha256:a5b411040beead47a228bde3b2241100454a6abde9df139ed087bd73fc0a4908", + "sha256:af22f3d8e228d84d1c0c44c1fbdeb80f97a15a0abe4f080960393a00db733b66", + "sha256:afd5ced4e5a96dac6725daeb5242a35494243f2239244fad10a90ce58b071d24", + "sha256:b9d45d1dbb9de84894cc50efece5b09939752a2d75aab3a8b0cef6f3a35ecd6b", + "sha256:bb894accfd16b867d8643fc2ba6c8617c78ba2828051e9a69511644ce86ce83e", + "sha256:c8c6c72d4a9f831f328efb1312642a1cafafaa88981d9ab76368d50d07d93cbe", + "sha256:cd7837b2b734ca72959a1caf3309457a318c934abef7a43a14bb984e574bbb9a", + "sha256:cdd9ec98f0063d93baeb01aad472a1a0840dee302842a2746a7a8e92968f9575", + "sha256:d1cfc92db6af1fd37a7bb58e55c8383b4aa1ba23d012bdbba26b4bcca45ac297", + "sha256:d1d2c6b7dd618c41e202c59c1413ef9b2c8e8a15f5039e344af64195459e3104", + "sha256:d2984cb6caaf05294b8466966627e80bf6c7afd273279077679cb010acb0e5ab", + "sha256:d58e8c51a7cf43090d124d5073bc29ab2755822181fcad978b12e144e5e5a4b3", + "sha256:d78f269e0c4fd365fc2992c00353e4530d274ba68f15e968d8bc3c69ce5f5244", + "sha256:dcfaf015b79d1f9f9c9fd0731a907407dc3e45769262d657d754c3a028586124", + "sha256:e44ccb93f30c75dfc0c3aa3ce38f33486a75ec9abadabd4e59f114994a9c4617", + "sha256:e509cbc488c735b43b5ffea175235cec24bbc57b227ef1acc691725beb230d1c" + ], + "markers": "python_version < '3.10'", + "version": "==1.26.1" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pandas": { + "hashes": [ + "sha256:0183cb04a057cc38fde5244909fca9826d5d57c4a5b7390c0cc3fa7acd9fa883", + "sha256:1fc87eac0541a7d24648a001d553406f4256e744d92df1df8ebe41829a915028", + "sha256:220b98d15cee0b2cd839a6358bd1f273d0356bf964c1a1aeb32d47db0215488b", + "sha256:2552bffc808641c6eb471e55aa6899fa002ac94e4eebfa9ec058649122db5824", + "sha256:315e19a3e5c2ab47a67467fc0362cb36c7c60a93b6457f675d7d9615edad2ebe", + "sha256:344021ed3e639e017b452aa8f5f6bf38a8806f5852e217a7594417fb9bbfa00e", + "sha256:375262829c8c700c3e7cbb336810b94367b9c4889818bbd910d0ecb4e45dc261", + "sha256:457d8c3d42314ff47cc2d6c54f8fc0d23954b47977b2caed09cd9635cb75388b", + "sha256:4aed257c7484d01c9a194d9a94758b37d3d751849c05a0050c087a358c41ad1f", + "sha256:530948945e7b6c95e6fa7aa4be2be25764af53fba93fe76d912e35d1c9ee46f5", + "sha256:5ae7e989f12628f41e804847a8cc2943d362440132919a69429d4dea1f164da0", + "sha256:71f510b0efe1629bf2f7c0eadb1ff0b9cf611e87b73cd017e6b7d6adb40e2b3a", + "sha256:73f219fdc1777cf3c45fde7f0708732ec6950dfc598afc50588d0d285fddaefc", + "sha256:8092a368d3eb7116e270525329a3e5c15ae796ccdf7ccb17839a73b4f5084a39", + "sha256:82ae615826da838a8e5d4d630eb70c993ab8636f0eff13cb28aafc4291b632b5", + "sha256:9608000a5a45f663be6af5c70c3cbe634fa19243e720eb380c0d378666bc7702", + "sha256:a40dd1e9f22e01e66ed534d6a965eb99546b41d4d52dbdb66565608fde48203f", + "sha256:b4f5a82afa4f1ff482ab8ded2ae8a453a2cdfde2001567b3ca24a4c5c5ca0db3", + "sha256:c009a92e81ce836212ce7aa98b219db7961a8b95999b97af566b8dc8c33e9519", + "sha256:c218796d59d5abd8780170c937b812c9637e84c32f8271bbf9845970f8c1351f", + "sha256:cc3cd122bea268998b79adebbb8343b735a5511ec14efb70a39e7acbc11ccbdc", + "sha256:d0d8fd58df5d17ddb8c72a5075d87cd80d71b542571b5f78178fb067fa4e9c72", + "sha256:e18bc3764cbb5e118be139b3b611bc3fbc5d3be42a7e827d1096f46087b395eb", + "sha256:e2b83abd292194f350bb04e188f9379d36b8dfac24dd445d5c87575f3beaf789", + "sha256:e7469271497960b6a781eaa930cba8af400dd59b62ec9ca2f4d31a19f2f91090", + "sha256:e9dbacd22555c2d47f262ef96bb4e30880e5956169741400af8b306bbb24a273", + "sha256:f6257b314fc14958f8122779e5a1557517b0f8e500cfb2bd53fa1f75a8ad0af2" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.2" + }, + "paramiko": { + "hashes": [ + "sha256:6a3777a961ac86dbef375c5f5b8d50014a1a96d0fd7f054a43bc880134b0ff77", + "sha256:b7bc5340a43de4287bbe22fe6de728aa2c22468b2a849615498dd944c2f275eb" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.3.1" + }, + "pathspec": { + "hashes": [ + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + ], + "markers": "python_version >= '3.7'", + "version": "==0.11.2" + }, + "platformdirs": { + "hashes": [ + "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", + "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + ], + "markers": "python_version >= '3.7'", + "version": "==3.11.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "pylint-report": { + "hashes": [ + "sha256:cd13c8500b1eac95fe6523da2c53f8a10fd63514f46bcf36a6b55b15c0efaecd", + "sha256:ff81d52ea818258d5539805dff1d409f82464b958c4dd30d2fa2c4bf5a6c3d62" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.4.0" + }, + "pynacl": { + "hashes": [ + "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", + "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", + "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", + "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", + "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", + "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", + "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", + "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", + "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", + "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" + ], + "markers": "python_version >= '3.6'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", + "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.1.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "pytz": { + "hashes": [ + "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", + "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + ], + "version": "==2023.3.post1" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "tomlkit": { + "hashes": [ + "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86", + "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + ], + "markers": "python_version < '3.11'", + "version": "==4.8.0" + } + } +} diff --git a/README.md b/README.md index 0b9ccd5..409db8c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,34 @@ +![SSHKey Tools](./assets/sshkey-tools-plain.svg) # sshkey-tools +[![PyPI version](https://badge.fury.io/py/sshkey-tools.svg)](https://badge.fury.io/py/sshkey-tools) +![Linting](https://github.com/scheiblingco/sshkey-tools/actions/workflows/linting.yml/badge.svg) +![Testing-Dev](https://github.com/scheiblingco/sshkey-tools/actions/workflows/dev_testing.yml/badge.svg) +![Testing-Build](https://github.com/scheiblingco/sshkey-tools/actions/workflows/deploy_testing.yml/badge.svg) +![Build](https://github.com/scheiblingco/sshkey-tools/actions/workflows/publish.yml/badge.svg) +[![CodeQL](https://github.com/scheiblingco/sshkey-tools/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/scheiblingco/sshkey-tools/actions/workflows/github-code-scanning/codeql) + + Python package for managing OpenSSH keypairs and certificates ([protocol.CERTKEYS](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys)). Supported functionality includes: +## Notice +The DSA algorithm has been deprecated and is removed in pyca/cryptography 41.x, meaning **version 0.9.* of this package will be the last to support DSA keys and certificates** for SSH. If there is any demand to reintroduce DSA support, please open an issue regarding this and we'll look into it. + +For now, **0.9.* will be restricted to version <41.1 of the cryptography package** and **0.10 will have its DSA support removed**. We've introduced a deprecation notice in version 0.9.3. + +### Background +The DSA algorithm is considered deprecated and will be removed in a future version. If possible, use RSA, [(ECDSA)](https://billatnapier.medium.com/ecdsa-weakness-where-nonces-are-reused-2be63856a01a) or ED25519 as a first-hand choice. + +### Notice from OpenSSH: +``` +OpenSSH 7.0 and greater similarly disable the ssh-dss (DSA) public key algorithm. It too is weak and we recommend against its use. It can be re-enabled using the HostKeyAlgorithms configuration option: sshd_config(5) HostKeyAlgorithms +``` + +[ECDSA has some flaws](https://billatnapier.medium.com/ecdsa-weakness-where-nonces-are-reused-2be63856a01a), especially when using short nonces or re-using nonces, it can still be used but exercise some caution in regards to nonces/re-signing identical data multiple times. + # Features ### SSH Keys -- Supports RSA, DSA, ECDSA and ED25519 keys +- Supports RSA, ECDSA and ED25519 keys - Import existing keys from file, string, byte data or [pyca/cryptography](https://github.com/pyca/cryptography) class - Generate new keys - Get public key from private keys @@ -13,7 +37,7 @@ Python package for managing OpenSSH keypairs and certificates ([protocol.CERTKEY - Generate fingerprint ### OpenSSH Certificates -- Supports RSA, DSA, ECDSA and ED25519 certificates +- Supports RSA, ECDSA and ED25519 certificates - Import existing certificates from file, string or bytes - Verify certificate signature against internal or separate public key - Create new certificates from CA private key and subject public key @@ -22,11 +46,7 @@ Python package for managing OpenSSH keypairs and certificates ([protocol.CERTKEY - Export certificates to file, string or bytes # Roadmap -- [x] Rewrite certificate field functionality for simpler usage -- [ ] Re-add functionality for changing RSA hash method -- [ ] Add CLI functionality -- [ ] Convert to/from putty format (keys only) - +See issues for planned features and fixes # Installation @@ -49,12 +69,18 @@ pip3 install ./ # Documentation You can find the full documentation at [scheiblingco.github.io/sshkey-tools/](https://scheiblingco.github.io/sshkey-tools/) +## Building the documentation +```bash +pdoc3 src/sshkey_tools/ -o docs --html +cp -rf docs/sshkey_tools/* docs/ +rm -rf docs/sshkey_tools +``` + ## SSH Keypairs (generating, loading, exporting) ```python # Import the certificate classes from sshkey_tools.keys import ( RsaPrivateKey, - DsaPrivateKey, EcdsaPrivateKey, Ed25519PrivateKey, EcdsaCurves @@ -69,7 +95,8 @@ rsa_priv = RsaPrivateKey.generate() rsa_priv = RsaPrivateKey.generate(2048) # Generate DSA keys (since SSH only supports 1024-bit keys, this is the default) -dsa_priv = DsaPrivateKey.generate() +# DEPRECATED +# dsa_priv = DsaPrivateKey.generate() # Generate ECDSA keys (The default curve is P521) ecdsa_priv = EcdsaPrivateKey.generate() @@ -124,6 +151,7 @@ b"\0xc\0a\........" The loaded private key objects can be used to sign bytestrings, and the public keys can be used to verify signatures on those ```python from sshkey_tools.keys import RsaPrivateKey, RsaPublicKey +from sshkey_tools.fields import RsaAlgs signable_data = b'This is a message that will be signed' @@ -133,6 +161,10 @@ pubkey = RsaPrivateKey.public_key # Sign the data signature = privkey.sign(signable_data) +# When using an RSA key for the signature, you can specify the hashing algorithm +# The default algorithm is SHA512 +signature = privkey.sign(signable_data, RsaAlgs.SHA512) + # Verify the signature (Throws exception if invalid) pubkey.verify(signable_data, signature) ``` @@ -308,6 +340,10 @@ certificate.sign() ``` ## Changelog +### 0.9.1 +- Updated documentation +- Fix for bug where exception would occur when trying to export a key without a comment set + ### 0.9 - Adjustments to certificate field handling for easier usage/syntax autocompletion - Updated testing diff --git a/assets/sshkey-tools-plain.svg b/assets/sshkey-tools-plain.svg new file mode 100644 index 0000000..0e73a40 --- /dev/null +++ b/assets/sshkey-tools-plain.svg @@ -0,0 +1,93 @@ + + + + + + + + + >_ + + + SSHKEY TOOLS + + diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 0000000..fb42284 --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/devcontainers/universal:2 + +RUN curl https://pyenv.run | bash \ + && echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc \ + && echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc \ + && echo 'eval "$(pyenv init -)"' >> ~/.bashrc + diff --git a/docs/cert.html b/docs/cert.html index bba49f6..d14897f 100644 --- a/docs/cert.html +++ b/docs/cert.html @@ -540,9 +540,13 @@

Raises

bytes(self.header), bytes(self.fields), bytes(self.footer) ) - def sign(self) -> bool: + def sign(self, **kwargs) -> bool: """Sign the certificate + Args: + **kwargs: Arguments to pass to the signature signing method + ex. hash_alg for RSA signatures + Raises: _EX.NotSignedException: The certificate could not be signed @@ -550,7 +554,7 @@

Raises

bool: Whether successful """ if self.can_sign(): - self.footer.signature.sign(data=self.get_signable()) + self.footer.signature.sign(data=self.get_signable(), **kwargs) return True raise _EX.NotSignedException("There was an error while signing the certificate") @@ -2164,9 +2168,13 @@

Inherited members

bytes(self.header), bytes(self.fields), bytes(self.footer) ) - def sign(self) -> bool: + def sign(self, **kwargs) -> bool: """Sign the certificate + Args: + **kwargs: Arguments to pass to the signature signing method + ex. hash_alg for RSA signatures + Raises: _EX.NotSignedException: The certificate could not be signed @@ -2174,7 +2182,7 @@

Inherited members

bool: Whether successful """ if self.can_sign(): - self.footer.signature.sign(data=self.get_signable()) + self.footer.signature.sign(data=self.get_signable(), **kwargs) return True raise _EX.NotSignedException("There was an error while signing the certificate") @@ -2672,10 +2680,16 @@

Returns

-def sign(self) ‑> bool +def sign(self, **kwargs) ‑> bool

Sign the certificate

+

Args

+
+
**kwargs
+
Arguments to pass to the signature signing method +ex. hash_alg for RSA signatures
+

Raises

_EX.NotSignedException
@@ -2690,9 +2704,13 @@

Returns

Expand source code -
def sign(self) -> bool:
+
def sign(self, **kwargs) -> bool:
     """Sign the certificate
 
+    Args:
+        **kwargs: Arguments to pass to the signature signing method
+                  ex. hash_alg for RSA signatures
+
     Raises:
         _EX.NotSignedException: The certificate could not be signed
 
@@ -2700,7 +2718,7 @@ 

Returns

bool: Whether successful """ if self.can_sign(): - self.footer.signature.sign(data=self.get_signable()) + self.footer.signature.sign(data=self.get_signable(), **kwargs) return True raise _EX.NotSignedException("There was an error while signing the certificate")
diff --git a/docs/fields.html b/docs/fields.html index 26ffaf1..7f078dd 100644 --- a/docs/fields.html +++ b/docs/fields.html @@ -66,7 +66,7 @@

Module sshkey_tools.fields

long_to_bytes, random_keyid, random_serial, - str_to_timedelta, + str_to_time_delta, ) NoneType = type(None) @@ -168,7 +168,7 @@

Module sshkey_tools.fields

""" Validates if the field is set when required """ - if self.DEFAULT == self.value is None: + if self.DEFAULT and self.value is None: return _EX.InvalidFieldDataException( f"{self.get_name()} is a required field" ) @@ -294,22 +294,23 @@

Module sshkey_tools.fields

DEFAULT = b"" @classmethod - def encode(cls, value: bytes) -> bytes: + def encode(cls, value: bytes, encoding: str = "utf-8") -> bytes: """ Encodes a string or bytestring into a packed byte string Args: value (Union[str, bytes]): The string/bytestring to encode - encoding (str): The encoding to user for the string + encoding (str): The optional encoding, not used when passing + a byte value. Returns: bytes: Packed byte string containing the source data """ cls.__validate_type__(value, True) - return pack(">I", len(value)) + ensure_bytestring(value) + return pack(">I", len(value)) + ensure_bytestring(value, encoding) @staticmethod - def decode(data: bytes) -> Tuple[bytes, bytes]: + def decode(data: bytes, encoding: str = None) -> Tuple[bytes, bytes]: """ Unpacks the next string from a packed byte string @@ -321,6 +322,10 @@

Module sshkey_tools.fields

string and remainder of the data """ length = unpack(">I", data[:4])[0] + 4 + + if encoding is not None: + return ensure_string(data[4:length], encoding), data[length:] + return ensure_bytestring(data[4:length]), data[length:] @@ -344,8 +349,7 @@

Module sshkey_tools.fields

Returns: bytes: Packed byte string containing the source data """ - cls.__validate_type__(value, True) - return BytestringField.encode(ensure_bytestring(value, encoding)) + return super().encode(value, encoding) @staticmethod def decode(data: bytes, encoding: str = "utf-8") -> Tuple[str, bytes]: @@ -359,9 +363,7 @@

Module sshkey_tools.fields

tuple(bytes, bytes): The next block of bytes from the packed byte string and remainder of the data """ - value, data = BytestringField.decode(data) - - return value.decode(encoding), data + return BytestringField.decode(data, encoding) class Integer32Field(CertificateField): @@ -498,9 +500,9 @@

Module sshkey_tools.fields

if isinstance(value, str): if value == "forever": - return Integer64Field.encode(MAX_INT64) + return Integer64Field.encode(MAX_INT64 - 1) - value = int(datetime.now() + str_to_timedelta(value)) + value = int((datetime.now() + str_to_time_delta(value)).timestamp()) if isinstance(value, datetime): value = int(value.timestamp()) @@ -779,7 +781,7 @@

Module sshkey_tools.fields

return True -class NonceField(StringField): +class NonceField(BytestringField): """ Contains the nonce for the certificate, randomly generated this protects the integrity of the private key, especially @@ -1321,8 +1323,11 @@

Module sshkey_tools.fields

def __table__(self) -> tuple: msg = "No signature" - if self.is_signed: + if self.is_signed and self.private_key is not None: msg = f"Signed with private key {self.private_key.get_fingerprint()}" + + if self.is_signed and self.private_key is None: + msg = "Signed with: See pubkey fingerprint above" return ("Signature", msg) @@ -1379,7 +1384,7 @@

Module sshkey_tools.fields

""" return self.private_key is not None - def sign(self, data: bytes) -> None: + def sign(self, data: bytes, **kwargs) -> None: """ Placeholder signing function """ @@ -1931,22 +1936,23 @@

Inherited members

DEFAULT = b"" @classmethod - def encode(cls, value: bytes) -> bytes: + def encode(cls, value: bytes, encoding: str = "utf-8") -> bytes: """ Encodes a string or bytestring into a packed byte string Args: value (Union[str, bytes]): The string/bytestring to encode - encoding (str): The encoding to user for the string + encoding (str): The optional encoding, not used when passing + a byte value. Returns: bytes: Packed byte string containing the source data """ cls.__validate_type__(value, True) - return pack(">I", len(value)) + ensure_bytestring(value) + return pack(">I", len(value)) + ensure_bytestring(value, encoding) @staticmethod - def decode(data: bytes) -> Tuple[bytes, bytes]: + def decode(data: bytes, encoding: str = None) -> Tuple[bytes, bytes]: """ Unpacks the next string from a packed byte string @@ -1958,6 +1964,10 @@

Inherited members

string and remainder of the data """ length = unpack(">I", data[:4])[0] + 4 + + if encoding is not None: + return ensure_string(data[4:length], encoding), data[length:] + return ensure_bytestring(data[4:length]), data[length:]

Ancestors

@@ -1968,6 +1978,7 @@

Subclasses

Class variables

@@ -1984,7 +1995,7 @@

Class variables

Static methods

-def decode(data: bytes) ‑> Tuple[bytes, bytes] +def decode(data: bytes, encoding: str = None) ‑> Tuple[bytes, bytes]

Unpacks the next string from a packed byte string

@@ -2002,7 +2013,7 @@

Returns

Expand source code
@staticmethod
-def decode(data: bytes) -> Tuple[bytes, bytes]:
+def decode(data: bytes, encoding: str = None) -> Tuple[bytes, bytes]:
     """
     Unpacks the next string from a packed byte string
 
@@ -2014,11 +2025,15 @@ 

Returns

string and remainder of the data """ length = unpack(">I", data[:4])[0] + 4 + + if encoding is not None: + return ensure_string(data[4:length], encoding), data[length:] + return ensure_bytestring(data[4:length]), data[length:]
-def encode(value: bytes) ‑> bytes +def encode(value: bytes, encoding: str = 'utf-8') ‑> bytes

Encodes a string or bytestring into a packed byte string

@@ -2027,7 +2042,8 @@

Args

value : Union[str, bytes]
The string/bytestring to encode
encoding : str
-
The encoding to user for the string
+
The optional encoding, not used when passing +a byte value.

Returns

@@ -2039,19 +2055,20 @@

Returns

Expand source code
@classmethod
-def encode(cls, value: bytes) -> bytes:
+def encode(cls, value: bytes, encoding: str = "utf-8") -> bytes:
     """
     Encodes a string or bytestring into a packed byte string
 
     Args:
         value (Union[str, bytes]): The string/bytestring to encode
-        encoding (str): The encoding to user for the string
+        encoding (str): The optional encoding, not used when passing
+                        a byte value.
 
     Returns:
         bytes: Packed byte string containing the source data
     """
     cls.__validate_type__(value, True)
-    return pack(">I", len(value)) + ensure_bytestring(value)
+ return pack(">I", len(value)) + ensure_bytestring(value, encoding)
@@ -2367,7 +2384,7 @@

Class variables

""" Validates if the field is set when required """ - if self.DEFAULT == self.value is None: + if self.DEFAULT and self.value is None: return _EX.InvalidFieldDataException( f"{self.get_name()} is a required field" ) @@ -2893,9 +2910,9 @@

Inherited members

if isinstance(value, str): if value == "forever": - return Integer64Field.encode(MAX_INT64) + return Integer64Field.encode(MAX_INT64 - 1) - value = int(datetime.now() + str_to_timedelta(value)) + value = int((datetime.now() + str_to_time_delta(value)).timestamp()) if isinstance(value, datetime): value = int(value.timestamp()) @@ -3033,9 +3050,9 @@

Returns

if isinstance(value, str): if value == "forever": - return Integer64Field.encode(MAX_INT64) + return Integer64Field.encode(MAX_INT64 - 1) - value = int(datetime.now() + str_to_timedelta(value)) + value = int((datetime.now() + str_to_time_delta(value)).timestamp()) if isinstance(value, datetime): value = int(value.timestamp()) @@ -4761,7 +4778,7 @@

Inherited members

  • StringField:
  • diff --git a/make_signature.sh b/make_signature.sh new file mode 100755 index 0000000..28dc276 --- /dev/null +++ b/make_signature.sh @@ -0,0 +1,9 @@ +#!/bin/bash +ssh-keygen -t rsa -b 4096 -f testkeys/id_rsa -N '' +ssh-keygen -t ecdsa -f testkeys/id_ecdsa -N '' +ssh-keygen -t ed25519 -f testkeys/id_ed25519 -N '' +echo "Hello World" | tee testkeys/rsa.txt | tee testkeys/ecdsa.txt | tee testkeys/ed25519.txt + +ssh-keygen -Y sign -n hello@world -f testkeys/id_rsa testkeys/rsa.txt +ssh-keygen -Y sign -n hello@world -f testkeys/id_ecdsa testkeys/ecdsa.txt +ssh-keygen -Y sign -n hello@world -f testkeys/id_ed25519 testkeys/ed25519.txt \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index f0e7818..4155983 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,4 +4,6 @@ coverage black pytest-cov faker -cprint \ No newline at end of file +cprint +pylint +pylint-report \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d7c755c..44996ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -click -cryptography -bcrypt -enum34 -PrettyTable -pytimeparse2 \ No newline at end of file +click>=7.1 +cryptography>=41.0 +bcrypt>=4.0 +enum34>=1.1 +PrettyTable>=3.1 +pytimeparse2>=1.4 \ No newline at end of file diff --git a/src/sshkey_tools/__version__.py b/src/sshkey_tools/__version__.py index 593c63e..f2c883d 100644 --- a/src/sshkey_tools/__version__.py +++ b/src/sshkey_tools/__version__.py @@ -9,7 +9,7 @@ # The version and build number # Without specifying a unique number, you cannot overwrite packages in the PyPi repo -__version__ = os.getenv("RELEASE_NAME", "0.9-dev" + os.getenv("GITHUB_RUN_ID", "")) +__version__ = os.getenv("RELEASE_NAME", "0.10.2-dev" + os.getenv("GITHUB_RUN_ID", "")) # Author and license information __author__ = "Lars Scheibling" diff --git a/src/sshkey_tools/cert.py b/src/sshkey_tools/cert.py index 4a893a2..0b63997 100644 --- a/src/sshkey_tools/cert.py +++ b/src/sshkey_tools/cert.py @@ -23,7 +23,6 @@ "ssh-rsa-cert-v01@openssh.com": ("RsaCertificate", "RsaPubkeyField"), "rsa-sha2-256-cert-v01@openssh.com": ("RsaCertificate", "RsaPubkeyField"), "rsa-sha2-512-cert-v01@openssh.com": ("RsaCertificate", "RsaPubkeyField"), - "ssh-dss-cert-v01@openssh.com": ("DsaCertificate", "DsaPubkeyField"), "ecdsa-sha2-nistp256-cert-v01@openssh.com": ( "EcdsaCertificate", "EcdsaPubkeyField", @@ -98,7 +97,8 @@ def get(self, name: str, default=None): if field: if isinstance(field, type): return field.DEFAULT - return field.value + if getattr(field, "value", False): + return field.value return field def getattrs(self) -> tuple: @@ -495,9 +495,13 @@ def get_signable(self) -> bytes: bytes(self.header), bytes(self.fields), bytes(self.footer) ) - def sign(self) -> bool: + def sign(self, **kwargs) -> bool: """Sign the certificate + Args: + **kwargs: Arguments to pass to the signature signing method + ex. hash_alg for RSA signatures + Raises: _EX.NotSignedException: The certificate could not be signed @@ -505,7 +509,7 @@ def sign(self) -> bool: bool: Whether successful """ if self.can_sign(): - self.footer.signature.sign(data=self.get_signable()) + self.footer.signature.sign(data=self.get_signable(), **kwargs) return True raise _EX.NotSignedException("There was an error while signing the certificate") @@ -573,9 +577,15 @@ class RsaCertificate(SSHCertificate): class DsaCertificate(SSHCertificate): - """The DSA Certificate class""" - - DEFAULT_KEY_TYPE = "ssh-dss-cert-v01@openssh.com" + """The DSA Certificate class (DEPRECATED)""" + + # pylint: disable=super-init-not-called + def __init__(self, *args, **kwargs): + """DEPRECATED CERTIFICATE CLASS""" + raise _EX.DeprecatedClassCalled( + "DSA certificates are deprecated and have been removed" + "since version 0.10 of sshkey-tools" + ) class EcdsaCertificate(SSHCertificate): diff --git a/src/sshkey_tools/exceptions.py b/src/sshkey_tools/exceptions.py index c71e508..7c8d336 100644 --- a/src/sshkey_tools/exceptions.py +++ b/src/sshkey_tools/exceptions.py @@ -107,3 +107,9 @@ class InvalidClassCallException(ValueError): """ Raised when trying to instantiate a parent class """ + + +class DeprecatedClassCalled(ValueError): + """ + Raised when trying to instantiate a deprecated class + """ diff --git a/src/sshkey_tools/fields.py b/src/sshkey_tools/fields.py index 72a029c..ed80b3a 100644 --- a/src/sshkey_tools/fields.py +++ b/src/sshkey_tools/fields.py @@ -16,8 +16,6 @@ from . import exceptions as _EX from .keys import ( - DsaPrivateKey, - DsaPublicKey, EcdsaPrivateKey, EcdsaPublicKey, Ed25519PrivateKey, @@ -53,25 +51,32 @@ SUBJECT_PUBKEY_MAP = { RsaPublicKey: "RsaPubkeyField", - DsaPublicKey: "DsaPubkeyField", EcdsaPublicKey: "EcdsaPubkeyField", Ed25519PublicKey: "Ed25519PubkeyField", } CA_SIGNATURE_MAP = { RsaPrivateKey: "RsaSignatureField", - DsaPrivateKey: "DsaSignatureField", EcdsaPrivateKey: "EcdsaSignatureField", Ed25519PrivateKey: "Ed25519SignatureField", } SIGNATURE_TYPE_MAP = { b"rsa": "RsaSignatureField", - b"dss": "DsaSignatureField", b"ecdsa": "EcdsaSignatureField", b"ed25519": "Ed25519SignatureField", } +PUBKEY_FIELD_TYPE_MAP = { + b"ssh-rsa": "RsaPubkeyField", + b"rsa-sha2-256": "RsaPubkeyField", + b"rsa-sha2-512": "RsaPubkeyField", + b"ecdsa-sha2-nistp256": "EcdsaPubkeyField", + b"ecdsa-sha2-nistp384": "EcdsaPubkeyField", + b"ecdsa-sha2-nistp521": "EcdsaPubkeyField", + b"ssh-ed25519": "Ed25519PubkeyField", +} + class CERT_TYPE(Enum): """ @@ -470,16 +475,32 @@ def encode(cls, value: Union[datetime, int, str]) -> bytes: cls.__validate_type__(value, True) if isinstance(value, str): - if value == "forever": - return Integer64Field.encode(MAX_INT64 - 1) - - value = int((datetime.now() + str_to_time_delta(value)).timestamp()) + value = cls.parse_string_value(value) if isinstance(value, datetime): value = int(value.timestamp()) return Integer64Field.encode(value) + @staticmethod + def parse_string_value(value: str) -> int: + """ + Parses a string value into an integer timestamp + + Args: + value (str): String value to parse + + Returns: + int: Integer timestamp + """ + if value == "forever": + return MAX_INT64 - 1 + + if value == "always": + return 1 + + return int((datetime.now() + str_to_time_delta(value)).timestamp()) + @staticmethod def decode(data: bytes) -> datetime: """Decodes a datetime object from a block of bytes @@ -502,7 +523,12 @@ def __validate_value__(self) -> Union[bool, Exception]: f"{self.get_name()} Could not validate value, invalid type" ) - check = self.value if isinstance(self.value, int) else self.value.timestamp() + check = self.value + if isinstance(check, str): + check = self.parse_string_value(check) + + if isinstance(check, datetime): + check = int(check.timestamp()) if check < MAX_INT64: return True @@ -727,7 +753,6 @@ class PubkeyTypeField(StringField): "ssh-rsa-cert-v01@openssh.com", "rsa-sha2-256-cert-v01@openssh.com", "rsa-sha2-512-cert-v01@openssh.com", - "ssh-dss-cert-v01@openssh.com", "ecdsa-sha2-nistp256-cert-v01@openssh.com", "ecdsa-sha2-nistp384-cert-v01@openssh.com", "ecdsa-sha2-nistp521-cert-v01@openssh.com", @@ -799,6 +824,27 @@ def __str__(self) -> str: ] ) + @classmethod + def decode(cls, data: bytes) -> "PublicKeyField": + """ + Decode from a packed pair of key type and key data + + Args: + data (bytes): Packed key type and data + + Raises: + _EX.InvalidKeyException: Invalid + + Returns: + PublicKeyField: A public key field + """ + pk_data, data = BytestringField.decode(data) + pk_type, pk_data = BytestringField.decode(pk_data) + + target_class = globals()[PUBKEY_FIELD_TYPE_MAP[pk_type]] + + return target_class.from_decode(pk_data)[0], data + @classmethod def encode(cls, value: PublicKey) -> bytes: """ @@ -867,11 +913,8 @@ class DsaPubkeyField(PublicKeyField): Holds the DSA Public Key for DSA Certificates """ - DEFAULT = None - DATA_TYPE = DsaPublicKey - @staticmethod - def decode(data: bytes) -> Tuple[DsaPublicKey, bytes]: + def decode(data: bytes): """ Decode the certificate field from a byte string starting with the encoded public key @@ -882,12 +925,7 @@ def decode(data: bytes) -> Tuple[DsaPublicKey, bytes]: Returns: Tuple[RsaPublicKey, bytes]: The PublicKey field and remainder of the data """ - p, data = MpIntegerField.decode(data) - q, data = MpIntegerField.decode(data) - g, data = MpIntegerField.decode(data) - y, data = MpIntegerField.decode(data) - - return DsaPublicKey.from_numbers(p=p, q=q, g=g, y=y), data + raise _EX.DeprecatedClassCalled("DSA is deprecated, use RSA or ECDSA instead") class EcdsaPubkeyField(PublicKeyField): @@ -1039,7 +1077,7 @@ class ValidAfterField(DateTimeField): """ DEFAULT = datetime.now() - DATA_TYPE = (datetime, int) + DATA_TYPE = (datetime, int, str) class ValidBeforeField(DateTimeField): @@ -1049,7 +1087,7 @@ class ValidBeforeField(DateTimeField): """ DEFAULT = datetime.now() + timedelta(minutes=10) - DATA_TYPE = (datetime, int) + DATA_TYPE = (datetime, int, str) def __validate_value__(self) -> Union[bool, Exception]: """ @@ -1064,13 +1102,15 @@ def __validate_value__(self) -> Union[bool, Exception]: ) super().__validate_value__() - check = ( - self.value - if isinstance(self.value, datetime) - else datetime.fromtimestamp(self.value) - ) - if check < datetime.now(): + check = self.value + if isinstance(check, str): + check = self.parse_string_value(check) + + if isinstance(check, datetime): + check = int(check.timestamp()) + + if check < int(datetime.now().timestamp()): return _EX.InvalidCertificateFieldException( "The certificate validity period is invalid" + " (expected a future datetime object or timestamp)" @@ -1294,9 +1334,12 @@ def __init__(self, private_key: PrivateKey = None, signature: bytes = None): def __table__(self) -> tuple: msg = "No signature" - if self.is_signed: + if self.is_signed and self.private_key is not None: msg = f"Signed with private key {self.private_key.get_fingerprint()}" + if self.is_signed and self.private_key is None: + msg = "Signed with: See pubkey fingerprint above" + return ("Signature", msg) @staticmethod @@ -1352,7 +1395,7 @@ def can_sign(self): """ return self.private_key is not None - def sign(self, data: bytes) -> None: + def sign(self, data: bytes, **kwargs) -> None: """ Placeholder signing function """ @@ -1466,16 +1509,14 @@ class DsaSignatureField(SignatureField): Creates and contains the DSA signature from an DSA Private Key """ - DEFAULT = None - DATA_TYPE = bytes - - def __init__( - self, private_key: DsaPrivateKey = None, signature: bytes = None - ) -> None: - super().__init__(private_key, signature) + # pylint: disable=super-init-not-called + def __init__(self, *args, **kwargs) -> None: + raise _EX.DeprecatedClassCalled( + "DSA signatures are deprecated and have been removed" + ) @classmethod - def encode(cls, value: bytes): + def encode(cls, value=None): """ Encodes the signature to a byte string @@ -1485,61 +1526,15 @@ def encode(cls, value: bytes): Returns: bytes: The encoded byte string """ - cls.__validate_type__(value, True) - - r, s = decode_dss_signature(value) - - return BytestringField.encode( - StringField.encode("ssh-dss") - + BytestringField.encode(long_to_bytes(r, 20) + long_to_bytes(s, 20)) - ) + cls() @staticmethod - def decode(data: bytes) -> Tuple[bytes, bytes]: - """ - Decodes a bytestring containing a signature - - Args: - data (bytes): The bytestring starting with the Signature - - Returns: - Tuple[ bytes, bytes ]: signature, remainder of the data - """ - signature, data = BytestringField.decode(data) - - signature = BytestringField.decode(BytestringField.decode(signature)[1])[0] - r = bytes_to_long(signature[:20]) - s = bytes_to_long(signature[20:]) - - signature = encode_dss_signature(r, s) - - return signature, data + def decode(data=None): + DsaSignatureField() @classmethod - def from_decode(cls, data: bytes) -> Tuple["DsaSignatureField", bytes]: - """ - Creates a signature field class from the encoded signature - - Args: - data (bytes): The bytestring starting with the Signature - - Returns: - Tuple[ DsaSignatureField, bytes ]: signature, remainder of the data - """ - signature, data = cls.decode(data) - - return cls(private_key=None, signature=signature), data - - # pylint: disable=unused-argument - def sign(self, data: bytes, **kwargs) -> None: - """ - Signs the provided data with the provided private key - - Args: - data (bytes): The data to be signed - """ - self.value = self.private_key.sign(data) - self.is_signed = True + def from_decode(cls, data=None): + cls() class EcdsaSignatureField(SignatureField): @@ -1717,7 +1712,89 @@ def sign(self, data: bytes, **kwargs) -> None: Args: data (bytes): The data to be signed hash_alg (RsaAlgs, optional): The RSA algorithm to use for hashing. - Defaults to RsaAlgs.SHA256. + Defaults to RsaAlgs.SHA256. """ self.value = self.private_key.sign(data) self.is_signed = True + + +class SshsigField(CertificateField): + """ + SSH Signature magic preamble field (static b'SSHSIG') + """ + DATA_TYPE = bytes + DEFAULT = b'SSHSIG' + @classmethod + def encode(cls, value: bytes) -> bytes: + """ + Encodes the SSH Signature magic preamble field + + Args: + value (bytes): The SSH Signature magic preamble field + + Returns: + bytes: The SSH Signature magic preamble field + """ + return b'SSHSIG' + + @classmethod + def decode(cls, data: bytes) -> Tuple[bytes, bytes]: + """ + Decodes the SSH Signature magic preamble field + + Args: + data (bytes): The SSH Signature magic preamble field + + Returns: + bytes: The SSH Signature magic preamble field + """ + return data[:6], data[6:] + + def __validate_value__(self) -> Union[bool, Exception]: + """ + Validates the contents of the field + """ + return True + +class SignatureVersionField(Integer32Field): + DATA_TYPE = int + DEFAULT = 1 + + def __validate_value__(self) -> Union[bool, Exception]: + """ + Validates the contents of the field + """ + if self.value != 1: + return _EX.InvalidCertificateFieldException( + "The certificate version is invalid" + ) + + return True + +class SignatureNamespaceField(StringField): + DATA_TYPE = (str, bytes) + DEFAULT = "" + +class SignatureNamespaceField(StringField): + DATA_TYPE = (str, bytes) + DEFAULT = "" + + def __validate_value__(self) -> Union[bool, Exception]: + if len(self.value) == 0: + return _EX.InvalidFieldDataException( + f"{self.get_name()} must be a non-empty string" + ) + + return True + +class SignatureHashAlgorithmField(StringField): + DATA_TYPE = (str, bytes) + DEFAULT = "sha512" + + def __validate_value__(self) -> Union[bool, Exception]: + if self.value not in ("sha256", "sha512"): + return _EX.InvalidFieldDataException( + f"{self.get_name()} must be one of 'sha256' or 'sha512'" + ) + + return True diff --git a/src/sshkey_tools/keys.py b/src/sshkey_tools/keys.py index 9143cbf..e3541a4 100644 --- a/src/sshkey_tools/keys.py +++ b/src/sshkey_tools/keys.py @@ -7,15 +7,12 @@ from typing import Union from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends.openssl.dsa import _DSAPrivateKey, _DSAPublicKey from cryptography.hazmat.backends.openssl.ec import ( _EllipticCurvePrivateKey, _EllipticCurvePublicKey, ) -from cryptography.hazmat.backends.openssl.ed25519 import ( - _Ed25519PrivateKey, - _Ed25519PublicKey, -) +from cryptography.hazmat.bindings import _rust as _RustBinding + from cryptography.hazmat.backends.openssl.rsa import _RSAPrivateKey, _RSAPublicKey from cryptography.hazmat.primitives import hashes as _HASHES from cryptography.hazmat.primitives import serialization as _SERIALIZATION @@ -26,24 +23,22 @@ from cryptography.hazmat.primitives.asymmetric import rsa as _RSA from . import exceptions as _EX -from .utils import ensure_bytestring, ensure_string +from .utils import ensure_bytestring, ensure_string, nullsafe_getattr from .utils import md5_fingerprint as _FP_MD5 from .utils import sha256_fingerprint as _FP_SHA256 from .utils import sha512_fingerprint as _FP_SHA512 PUBKEY_MAP = { _RSAPublicKey: "RsaPublicKey", - _DSAPublicKey: "DsaPublicKey", _EllipticCurvePublicKey: "EcdsaPublicKey", - _Ed25519PublicKey: "Ed25519PublicKey", + _RustBinding.openssl.ed25519.Ed25519PublicKey: "Ed25519PublicKey", } PRIVKEY_MAP = { _RSAPrivateKey: "RsaPrivateKey", - _DSAPrivateKey: "DsaPrivateKey", _EllipticCurvePrivateKey: "EcdsaPrivateKey", # trunk-ignore(gitleaks/generic-api-key) - _Ed25519PrivateKey: "Ed25519PrivateKey", + _RustBinding.openssl.ed25519.Ed25519PrivateKey: "Ed25519PrivateKey", } ECDSA_HASHES = { @@ -130,11 +125,14 @@ def __init__( _SERIALIZATION.PublicFormat.OpenSSH, ] + # Ensure comment is not None + self.comment = nullsafe_getattr(self, "comment", "") + @classmethod def from_class( cls, key_class: PubkeyClasses, - comment: Union[str, bytes] = None, + comment: Union[str, bytes] = "", key_type: Union[str, bytes] = None, ) -> "PublicKey": """ @@ -266,7 +264,7 @@ def to_string(self, encoding: str = "utf-8") -> str: return " ".join( [ ensure_string(self.serialize(), encoding), - ensure_string(getattr(self, "comment", ""), encoding), + ensure_string(nullsafe_getattr(self, "comment", ""), encoding), ] ) @@ -595,60 +593,18 @@ class DsaPublicKey(PublicKey): Class for holding DSA public keys """ - def __init__( - self, - key: _DSA.DSAPublicKey, - comment: Union[str, bytes] = None, - key_type: Union[str, bytes] = None, - serialized: bytes = None, - ): - super().__init__( - key=key, - comment=comment, - key_type=key_type, - public_numbers=key.public_numbers(), - serialized=serialized, + # pylint: disable=super-init-not-called + def __init__(self, key=None, comment=None, key_type=None, serialized=None): + raise _EX.DeprecatedClassCalled( + "SSH DSA keys and certificates are deprecated " + "and are removed since version 0.10 of sshkey-tools", ) - self.parameters = key.parameters().parameter_numbers() @classmethod - # pylint: disable=invalid-name + # pylint: disable=invalid-name,unused-argument def from_numbers(cls, p: int, q: int, g: int, y: int) -> "DsaPublicKey": - """ - Create a DSA public key from public numbers and parameters - - Args: - p (int): P parameter, the prime modulus - q (int): Q parameter, the order of the subgroup - g (int): G parameter, the generator - y (int): The public number Y - - Returns: - DsaPublicKey: An instance of DsaPublicKey - """ - return cls( - key=_DSA.DSAPublicNumbers( - y=y, parameter_numbers=_DSA.DSAParameterNumbers(p=p, q=q, g=g) - ).public_key() - ) - - def verify(self, data: bytes, signature: bytes) -> None: - """ - Verifies a signature - - Args: - data (bytes): The data to verify - signature (bytes): The signature to verify - - Raises: - Raises an sshkey_tools.exceptions.InvalidSignatureException if the signature is invalid - """ - try: - return self.key.verify(signature, data, _HASHES.SHA1()) - except InvalidSignature: - raise _EX.InvalidSignatureException( - "The signature is invalid for the given data" - ) from InvalidSignature + """Deprecated""" + return cls() class DsaPrivateKey(PrivateKey): @@ -656,16 +612,16 @@ class DsaPrivateKey(PrivateKey): Class for holding DSA private keys """ - def __init__(self, key: _DSA.DSAPrivateKey): - super().__init__( - key=key, - public_key=DsaPublicKey(key.public_key()), - private_numbers=key.private_numbers(), + # pylint: disable=super-init-not-called + def __init__(self, key=None): + raise _EX.DeprecatedClassCalled( + "SSH DSA keys and certificates are deprecated " + "and are removed since version 0.10 of sshkey-tools", ) @classmethod - # pylint: disable=invalid-name,too-many-arguments - def from_numbers(cls, p: int, q: int, g: int, y: int, x: int) -> "DsaPrivateKey": + # pylint: disable=invalid-name,too-many-arguments,unused-argument + def from_numbers(cls, p, q, g, y, x): """ Creates a new DsaPrivateKey object from parameters and public/private numbers @@ -679,14 +635,7 @@ def from_numbers(cls, p: int, q: int, g: int, y: int, x: int) -> "DsaPrivateKey" Returns: _type_: _description_ """ - return cls( - key=_DSA.DSAPrivateNumbers( - public_numbers=_DSA.DSAPublicNumbers( - y=y, parameter_numbers=_DSA.DSAParameterNumbers(p=p, q=q, g=g) - ), - x=x, - ).private_key() - ) + return cls() @classmethod def generate(cls) -> "DsaPrivateKey": @@ -697,19 +646,7 @@ def generate(cls) -> "DsaPrivateKey": Returns: DsaPrivateKey: An instance of DsaPrivateKey """ - return cls.from_class(_DSA.generate_private_key(key_size=1024)) - - def sign(self, data: bytes): - """ - Signs a block of data and returns the signature - - Args: - data (bytes): Block of byte data to sign - - Returns: - bytes: The signature bytes - """ - return self.key.sign(data, _HASHES.SHA1()) + return cls() class EcdsaPublicKey(PublicKey): diff --git a/src/sshkey_tools/signatures.py b/src/sshkey_tools/signatures.py new file mode 100644 index 0000000..9b6aed2 --- /dev/null +++ b/src/sshkey_tools/signatures.py @@ -0,0 +1,241 @@ +# Format: + +# byte[6] MAGIC_PREAMBLE (SSHSIG) +# uint32 SIG_VERSION (0x01) +# string publickey +# string namespace +# string reserved +# string hash_algorithm +# string signature +from base64 import b64decode, b64encode +from .cert import Fieldset, dataclass, Union +from prettytable import PrettyTable +from .utils import concat_to_bytestring, concat_to_string, ensure_bytestring +from . import ( + fields as _FIELD, + keys as _KEY, + exceptions as _EX, + utils as _U +) + +@dataclass +class SignatureFieldset(Fieldset): + """Fields for SSH Signature""" + + DECODE_ORDER = [ + "magic_preamble", + "sig_version", + "public_key", + "namespace", + "reserved", + "hash_algorithm", + "signature" + ] + + magic_preamble: _FIELD.SshsigField = _FIELD.SshsigField.factory + sig_version: _FIELD.SignatureVersionField = _FIELD.SignatureVersionField.factory + public_key: _FIELD.PublicKeyField = _FIELD.PublicKeyField.factory + + namespace: _FIELD.SignatureNamespaceField = _FIELD.StringField.factory + reserved: _FIELD.ReservedField = _FIELD.ReservedField.factory + hash_algorithm: _FIELD.SignatureHashAlgorithmField = _FIELD.SignatureHashAlgorithmField.factory + signature: _FIELD.SignatureField = _FIELD.SignatureField.factory + + def __bytes__(self): + return concat_to_bytestring( + bytes(self.magic_preamble), + bytes(self.namespace), + bytes(self.reserved), + bytes(self.hash_algorithm) + ) + + def format_pubkey(self): + pubkey = self.public_key.value.to_string().split(' ') + + return _FIELD.StringField.encode(concat_to_bytestring( + pubkey[0], + ' ', + b64decode(pubkey[1])) + ) + + + # return _FIELD.BytestringField.encode(concat_to_bytestring( + # _FIELD.StringField.encode(pubkey[0]), + # _FIELD.StringField.encode(b64decode(pubkey[1])) + # )) + + def bytes_out(self): + return concat_to_bytestring( + bytes(self.magic_preamble), + bytes(self.sig_version), + _FIELD.StringField.encode(self.public_key.value.raw_bytes()), + bytes(self.namespace), + bytes(self.reserved), + bytes(self.hash_algorithm), + bytes(self.signature) + ) + +class SSHSignature: + """ + General class for SSH Signatures, used for loading and parsing. + """ + data: bytes = None + + def __init__( + self, signer_privkey: _KEY.PrivateKey = None, + fields: SignatureFieldset = SignatureFieldset + ): + self.fields = fields() if isinstance(fields, type) else fields + + if signer_privkey is not None: + self.fields.replace_field( + "signature", _FIELD.SignatureField.from_object(signer_privkey) + ) + self.fields.replace_field("public_key", _FIELD.PublicKeyField.from_object(signer_privkey.public_key)) + + if issubclass(type(self.fields.public_key.value), type(self.fields.public_key)): + self.fields.public_key = self.fields.public_key.value + + @classmethod + def from_file(cls, path: str, encoding: str = 'none') -> "SSHSignature": + """ + Loads an existing SSH Signature from a file + + Args: + path (str): The path to the file + encoding (str, optional): The encoding of the file. None will load the byte content directly. Defaults to 'utf-8'. + + Returns: + SSHSignature: SSH Signature Object + """ + with open(path, 'rb' if encoding == 'none' else 'r') as f: + data = f.read() + + return cls.from_string(data, encoding if encoding != 'none' else None) + + @classmethod + def from_string(cls, data: Union[str, bytes], encoding: str = 'utf-8') -> "SSHSignature": + """ + Loads an existing SSH Signature from file contents/string + + Args: + data (str): The normalized string data from the .sig-file + + Returns: + SSHSignature: The parsed SSH Signature + """ + if isinstance(data, str): + data = data.encode(encoding) + + if b'BEGIN SSH SIGNATURE' in data: + data = data.replace(b'-----BEGIN SSH SIGNATURE-----\n', b'') + + if b'END SSH SIGNATURE' in data: + data = data.replace(b'-----END SSH SIGNATURE-----', b'') + + data = data.strip(b"\n \t") + + return cls.decode(b64decode(data)) + + @classmethod + def decode(cls, data: bytes) -> "SSHSignature": + """ + Loads an existing SSH Signature from byte contents + + Args: + data (bytes): The normalized byte data from the .sig-file + + Returns: + SSHSignature: The parsed SSH Signature + """ + sig_fields, data = SignatureFieldset.decode(data) + return cls(fields=sig_fields) + + def get_signable(self, data: Union[str, bytes]) -> bytes: + """ + Returns the signable data for the signature or verification + + Returns: + bytes: The signable data + """ + hash = b"" + if self.fields.hash_algorithm.value == "sha256": + hash = _U.sha256_hash(ensure_bytestring(data)) + elif self.fields.hash_algorithm.value == "sha512": + hash = _U.sha512_hash(ensure_bytestring(data)) + else: + raise _EX.InvalidHashAlgorithmException( + f"Unknown hash algorithm {self.fields.hash_algorithm}" + ) + + return bytes(self.fields) + _FIELD.StringField.encode(hash) + + def get_signable_file(self, path: str) -> bytes: + """ + Loads the signable content from a file. + Will be loaded as bytes without encoding. + + Args: + path (str): Path to the file + + Returns: + bytes: The signable data from the file + """ + with open(path, 'rb') as f: + data = f.read() + + return self.get_signable(data) + + def __str__(self) -> str: + table = PrettyTable(["Field", "Value"]) + + for item in (self.header, self.fields, self.footer): + for row in item.__table__(): + table.add_row(row) + + return str(table) + + def get(self, field: str): + if field in self.fields.getattrs(): + return self.fields.get(field, None) + + raise _EX.InvalidCertificateFieldException(f"Unknown field {field}") + + def set(self, field: str, data): + if field in self.fields.getattrs(): + self.fields.set(field, data) + + raise _EX.InvalidCertificateFieldException(f"Unknown field {field}") + + def verify( + self, data, public_key: _KEY.PublicKey = None, raise_on_error: bool = False + ) -> bool: + if not public_key: + public_key = self.fields.get('public_key', None) + + public_key.verify( + self.get_signable(data), + self.fields.signature.value + ) + + def sign(self, data: Union[str, bytes]): + signable = self.get_signable(data) + self.fields.signature.sign(signable) + + def sign_file(self, path: str): + signable = self.get_signable_file(path) + self.fields.signature.sign(signable) + + def to_string(self): + content = self.fields.bytes_out() + content = b64encode(content) + file_content = b"-----BEGIN SSH SIGNATURE-----\n" + file_content += b''.join([content[i:i+70] + b"\n" for i in range(0, len(content), 70)]) + file_content += b"-----END SSH SIGNATURE-----" + + return file_content + + def to_file(self, path: str): + with open(path, 'wb') as f: + f.write(self.to_string()) + \ No newline at end of file diff --git a/src/sshkey_tools/utils.py b/src/sshkey_tools/utils.py index 0d98234..0e4be3a 100644 --- a/src/sshkey_tools/utils.py +++ b/src/sshkey_tools/utils.py @@ -198,6 +198,29 @@ def md5_fingerprint(data: bytes, prefix: bool = True) -> str: a + b for a, b in zip(digest[::2], digest[1::2]) ) +def sha256_hash(data: bytes) -> str: + """ + Returns a SHA256 hash of the given data. + + Args: + data (bytes): The data to hash + + Returns: + str: The hash + """ + return hl.sha256(data).digest() + +def sha512_hash(data: bytes) -> str: + """ + Returns a SHA512 hash of the given data. + + Args: + data (bytes): The data to hash + + Returns: + str: The hash + """ + return hl.sha512(data).digest() def sha256_fingerprint(data: bytes, prefix: bool = True) -> str: """ @@ -233,6 +256,23 @@ def sha512_fingerprint(data: bytes, prefix: bool = True) -> str: ) +def nullsafe_getattr(obj, attr: str, default): + """ + Null-safe getattr, ensuring the result is not None. + If the result is None, the default value is returned instead. + + Args: + obj: The object + attr: The attribute to get + default: The default value + """ + att = getattr(obj, attr, default) + if att is None: + att = default + + return att + + def join_dicts(*dicts) -> dict: """ Joins two or more dictionaries together. diff --git a/tests/test_certificates.py b/tests/test_certificates.py index 682c0a3..62976a4 100644 --- a/tests/test_certificates.py +++ b/tests/test_certificates.py @@ -16,14 +16,13 @@ import src.sshkey_tools.fields as _FIELD import src.sshkey_tools.keys as _KEY -CERTIFICATE_TYPES = ["rsa", "dsa", "ecdsa", "ed25519"] +CERTIFICATE_TYPES = ["rsa", "ecdsa", "ed25519"] class TestCertificateFields(unittest.TestCase): def setUp(self): self.faker = faker.Faker() self.rsa_key = _KEY.RsaPrivateKey.generate(1024) - self.dsa_key = _KEY.DsaPrivateKey.generate() self.ecdsa_key = _KEY.EcdsaPrivateKey.generate() self.ed25519_key = _KEY.Ed25519PrivateKey.generate() @@ -232,10 +231,6 @@ def test_pubkey_type_field(self): "rsa-sha2-512-cert-v01@openssh.com", b"\x00\x00\x00!rsa-sha2-512-cert-v01@openssh.com", ), - ( - "ssh-dss-cert-v01@openssh.com", - b"\x00\x00\x00\x1cssh-dss-cert-v01@openssh.com", - ), ( "ecdsa-sha2-nistp256-cert-v01@openssh.com", b"\x00\x00\x00(ecdsa-sha2-nistp256-cert-v01@openssh.com", @@ -281,17 +276,14 @@ def test_nonce_field(self): def test_pubkey_class_assignment(self): rsa_field = _FIELD.PublicKeyField.from_object(self.rsa_key.public_key) - dsa_field = _FIELD.PublicKeyField.from_object(self.dsa_key.public_key) ecdsa_field = _FIELD.PublicKeyField.from_object(self.ecdsa_key.public_key) ed25519_field = _FIELD.PublicKeyField.from_object(self.ed25519_key.public_key) self.assertIsInstance(rsa_field, _FIELD.RsaPubkeyField) - self.assertIsInstance(dsa_field, _FIELD.DsaPubkeyField) self.assertIsInstance(ecdsa_field, _FIELD.EcdsaPubkeyField) self.assertIsInstance(ed25519_field, _FIELD.Ed25519PubkeyField) self.assertTrue(rsa_field.validate()) - self.assertTrue(dsa_field.validate()) self.assertTrue(ecdsa_field.validate()) self.assertTrue(ed25519_field.validate()) @@ -316,9 +308,6 @@ def assertPubkeyOutput(self, key_class, *opts): def test_rsa_pubkey_output(self): self.assertPubkeyOutput(_KEY.RsaPrivateKey, 1024) - def test_dsa_pubkey_output(self): - self.assertPubkeyOutput(_KEY.DsaPrivateKey) - def test_ecdsa_pubkey_output(self): self.assertPubkeyOutput(_KEY.EcdsaPrivateKey) @@ -551,12 +540,10 @@ def setUp(self): self.faker = faker.Faker() self.rsa_ca = _KEY.RsaPrivateKey.generate(1024) - self.dsa_ca = _KEY.DsaPrivateKey.generate() self.ecdsa_ca = _KEY.EcdsaPrivateKey.generate() self.ed25519_ca = _KEY.Ed25519PrivateKey.generate() self.rsa_user = _KEY.RsaPrivateKey.generate(1024).public_key - self.dsa_user = _KEY.DsaPrivateKey.generate().public_key self.ecdsa_user = _KEY.EcdsaPrivateKey.generate().public_key self.ed25519_user = _KEY.Ed25519PrivateKey.generate().public_key @@ -581,12 +568,10 @@ def tearDown(self): def test_cert_type_assignment(self): rsa_cert = _CERT.SSHCertificate.create(self.rsa_user) - dsa_cert = _CERT.SSHCertificate.create(self.dsa_user) ecdsa_cert = _CERT.SSHCertificate.create(self.ecdsa_user) ed25519_cert = _CERT.SSHCertificate.create(self.ed25519_user) self.assertIsInstance(rsa_cert, _CERT.RsaCertificate) - self.assertIsInstance(dsa_cert, _CERT.DsaCertificate) self.assertIsInstance(ecdsa_cert, _CERT.EcdsaCertificate) self.assertIsInstance(ed25519_cert, _CERT.Ed25519Certificate) diff --git a/tests/test_keypairs.py b/tests/test_keypairs.py index 6b60fdb..69cc202 100644 --- a/tests/test_keypairs.py +++ b/tests/test_keypairs.py @@ -6,15 +6,12 @@ import shutil import unittest -from cryptography.hazmat.primitives.asymmetric import dsa as _DSA from cryptography.hazmat.primitives.asymmetric import ec as _EC from cryptography.hazmat.primitives.asymmetric import ed25519 as _ED25519 from cryptography.hazmat.primitives.asymmetric import rsa as _RSA import src.sshkey_tools.exceptions as _EX from src.sshkey_tools.keys import ( - DsaPrivateKey, - DsaPublicKey, EcdsaCurves, EcdsaPrivateKey, EcdsaPublicKey, @@ -30,7 +27,6 @@ class KeypairMethods(unittest.TestCase): def generateClasses(self): self.rsa_key = RsaPrivateKey.generate(2048) - self.dsa_key = DsaPrivateKey.generate() self.ecdsa_key = EcdsaPrivateKey.generate(EcdsaCurves.P256) self.ed25519_key = Ed25519PrivateKey.generate() @@ -45,9 +41,6 @@ def generateFiles(self, folder): os.system( f'ssh-keygen -t rsa -b 2048 -f tests/{folder}/rsa_key_sshkeygen -N "password" > /dev/null 2>&1' ) - os.system( - f'ssh-keygen -t dsa -b 1024 -f tests/{folder}/dsa_key_sshkeygen -N "" > /dev/null 2>&1' - ) os.system( f'ssh-keygen -t ecdsa -b 256 -f tests/{folder}/ecdsa_key_sshkeygen -N "" > /dev/null 2>&1' ) @@ -105,20 +98,20 @@ def test_fail_assertions(self): RsaPrivateKey.from_file( f"tests/{self.folder}/rsa_key_sshkeygen", "password" ), - DsaPrivateKey.from_file(f"tests/{self.folder}/dsa_key_sshkeygen"), + EcdsaPrivateKey.from_file(f"tests/{self.folder}/ecdsa_key_sshkeygen"), ) with self.assertRaises(AssertionError): self.assertEqualPublicKeys( RsaPublicKey, RsaPublicKey.from_file(f"tests/{self.folder}/rsa_key_sshkeygen.pub"), - DsaPublicKey.from_file(f"tests/{self.folder}/dsa_key_sshkeygen.pub"), + EcdsaPublicKey.from_file(f"tests/{self.folder}/ecdsa_key_sshkeygen.pub"), ) with self.assertRaises(AssertionError): self.assertEqualKeyFingerprint( f"tests/{self.folder}/rsa_key_sshkeygen", - f"tests/{self.folder}/dsa_key_sshkeygen", + f"tests/{self.folder}/ecdsa_key_sshkeygen", ) def test_successful_assertions(self): @@ -128,12 +121,6 @@ def test_successful_assertions(self): f"tests/{self.folder}/rsa_key_sshkeygen.pub", ) - self.assertTrue(os.path.isfile(f"tests/{self.folder}/dsa_key_sshkeygen")) - self.assertEqualKeyFingerprint( - f"tests/{self.folder}/dsa_key_sshkeygen", - f"tests/{self.folder}/dsa_key_sshkeygen.pub", - ) - self.assertTrue(os.path.isfile(f"tests/{self.folder}/ecdsa_key_sshkeygen")) self.assertEqualKeyFingerprint( f"tests/{self.folder}/ecdsa_key_sshkeygen", @@ -174,21 +161,6 @@ def test_rsa_incorrect_keysize(self): with self.assertRaises(ValueError): RsaPrivateKey.generate(256) - def test_dsa(self): - - key = DsaPrivateKey.generate() - - assert isinstance(key, DsaPrivateKey) - assert isinstance(key, PrivateKey) - assert isinstance(key.key, _DSA.DSAPrivateKey) - assert isinstance(key.private_numbers, _DSA.DSAPrivateNumbers) - - assert isinstance(key.public_key, DsaPublicKey) - assert isinstance(key.public_key, PublicKey) - assert isinstance(key.public_key.key, _DSA.DSAPublicKey) - assert isinstance(key.public_key.public_numbers, _DSA.DSAPublicNumbers) - assert isinstance(key.public_key.parameters, _DSA.DSAParameterNumbers) - def test_ecdsa(self): curves = [EcdsaCurves.P256, EcdsaCurves.P384, EcdsaCurves.P521] @@ -297,49 +269,6 @@ def test_rsa_files(self): f"tests/{self.folder}/rsa_key_sshkeygen.pub", ) - def test_dsa_files(self): - parent = PrivateKey.from_file(f"tests/{self.folder}/dsa_key_sshkeygen") - child = DsaPrivateKey.from_file(f"tests/{self.folder}/dsa_key_sshkeygen") - - parent_pub = PublicKey.from_file(f"tests/{self.folder}/dsa_key_sshkeygen.pub") - child_pub = DsaPublicKey.from_file(f"tests/{self.folder}/dsa_key_sshkeygen.pub") - - parent.to_file(f"tests/{self.folder}/dsa_key_saved_parent") - child.to_file(f"tests/{self.folder}/dsa_key_saved_child") - - parent_pub.to_file(f"tests/{self.folder}/dsa_key_saved_parent.pub") - child_pub.to_file(f"tests/{self.folder}/dsa_key_saved_child.pub") - - self.assertEqualPrivateKeys(DsaPrivateKey, DsaPublicKey, parent, child) - - self.assertEqualPublicKeys(DsaPublicKey, parent_pub, child_pub) - - self.assertEqualPublicKeys(DsaPublicKey, parent.public_key, child_pub) - - self.assertEqualKeyFingerprint( - f"tests/{self.folder}/dsa_key_sshkeygen", - f"tests/{self.folder}/dsa_key_saved_parent", - ) - - self.assertEqualKeyFingerprint( - f"tests/{self.folder}/dsa_key_sshkeygen.pub", - f"tests/{self.folder}/dsa_key_saved_parent.pub", - ) - - self.assertEqualKeyFingerprint( - f"tests/{self.folder}/dsa_key_saved_parent", - f"tests/{self.folder}/dsa_key_saved_child", - ) - - self.assertEqualKeyFingerprint( - f"tests/{self.folder}/dsa_key_saved_parent.pub", - f"tests/{self.folder}/dsa_key_sshkeygen.pub", - ) - self.assertEqualKeyFingerprint( - f"tests/{self.folder}/dsa_key_saved_parent.pub", - f"tests/{self.folder}/dsa_key_sshkeygen.pub", - ) - def test_ecdsa_files(self): parent = PrivateKey.from_file(f"tests/{self.folder}/ecdsa_key_sshkeygen") child = EcdsaPrivateKey.from_file(f"tests/{self.folder}/ecdsa_key_sshkeygen") @@ -438,7 +367,6 @@ def test_ed25519_files(self): class TestFromClass(KeypairMethods): def setUp(self): self.rsa_key = _RSA.generate_private_key(public_exponent=65537, key_size=2048) - self.dsa_key = _DSA.generate_private_key(key_size=1024) self.ecdsa_key = _EC.generate_private_key(curve=_EC.SECP384R1()) self.ed25519_key = _ED25519.Ed25519PrivateKey.generate() @@ -457,12 +385,6 @@ def test_rsa_from_class(self): self.assertEqualPrivateKeys(RsaPrivateKey, RsaPublicKey, parent, child) - def test_dsa_from_class(self): - parent = PrivateKey.from_class(self.dsa_key) - child = DsaPrivateKey.from_class(self.dsa_key) - - self.assertEqualPrivateKeys(DsaPrivateKey, DsaPublicKey, parent, child) - def test_ecdsa_from_class(self): parent = PrivateKey.from_class(self.ecdsa_key) child = EcdsaPrivateKey.from_class(self.ecdsa_key) @@ -513,30 +435,7 @@ def test_rsa_from_numbers(self): self.assertEqual(self.rsa_key.private_numbers.d, from_numbers.private_numbers.d) - def test_dsa_from_numbers(self): - from_numbers = DsaPrivateKey.from_numbers( - p=self.dsa_key.public_key.parameters.p, - q=self.dsa_key.public_key.parameters.q, - g=self.dsa_key.public_key.parameters.g, - y=self.dsa_key.public_key.public_numbers.y, - x=self.dsa_key.private_numbers.x, - ) - - from_numbers_pub = DsaPublicKey.from_numbers( - p=self.dsa_key.public_key.parameters.p, - q=self.dsa_key.public_key.parameters.q, - g=self.dsa_key.public_key.parameters.g, - y=self.dsa_key.public_key.public_numbers.y, - ) - - self.assertEqualPrivateKeys( - DsaPrivateKey, DsaPublicKey, self.dsa_key, from_numbers - ) - - self.assertEqualPublicKeys( - DsaPublicKey, from_numbers_pub, self.dsa_key.public_key - ) - + def test_ecdsa_from_numbers(self): from_numbers = EcdsaPrivateKey.from_numbers( curve=self.ecdsa_key.public_key.key.curve, @@ -599,16 +498,6 @@ def test_rsa_fingerprint(self): self.assertEqual(key.get_fingerprint(), sshkey_fingerprint) - def test_dsa_fingerprint(self): - key = DsaPrivateKey.from_file( - f"tests/{self.folder}/dsa_key_sshkeygen", - ) - - with os.popen(f"ssh-keygen -lf tests/{self.folder}/dsa_key_sshkeygen") as cmd: - sshkey_fingerprint = cmd.read().split(" ")[1] - - self.assertEqual(key.get_fingerprint(), sshkey_fingerprint) - def test_ecdsa_fingerprint(self): key = EcdsaPrivateKey.from_file( f"tests/{self.folder}/ecdsa_key_sshkeygen", @@ -646,15 +535,6 @@ def test_rsa_signature(self): with self.assertRaises(_EX.InvalidSignatureException): self.rsa_key.public_key.verify(data, signature + b"\x00") - def test_dsa_signature(self): - data = b"\x00" + os.urandom(32) + b"\x00" - signature = self.dsa_key.sign(data) - - self.assertIsNone(self.dsa_key.public_key.verify(data, signature)) - - with self.assertRaises(_EX.InvalidSignatureException): - self.dsa_key.public_key.verify(data, signature + b"\x00") - def test_ecdsa_signature(self): data = b"\x00" + os.urandom(32) + b"\x00" signature = self.ecdsa_key.sign(data) diff --git a/validate_signatures.py b/validate_signatures.py new file mode 100644 index 0000000..225e6a3 --- /dev/null +++ b/validate_signatures.py @@ -0,0 +1,71 @@ +from src.sshkey_tools import ( + fields as _F, + keys as _K, + exceptions as _E, + signatures as _S +) + +# Load public and private keys +rsa_priv = _K.PrivateKey.from_file('testkeys/id_rsa') +ecdsa_priv = _K.PrivateKey.from_file('testkeys/id_ecdsa') +ed25519_priv = _K.PrivateKey.from_file('testkeys/id_ed25519') +rsa_pub = rsa_priv.public_key +ecdsa_pub = ecdsa_priv.public_key +ed25519_pub = ed25519_priv.public_key + +# Load externally created signatures +rsa_sign = _S.SSHSignature.from_file('testkeys/rsa.txt.sig') +ecdsa_sign = _S.SSHSignature.from_file('testkeys/ecdsa.txt.sig') +ed25519_sign = _S.SSHSignature.from_file('testkeys/ed25519.txt.sig') + +# Load the data used for the signatures +rsa_data = open('testkeys/rsa.txt', 'rb').read() +ecdsa_data = open('testkeys/ecdsa.txt', 'rb').read() +ed25519_data = open('testkeys/ed25519.txt', 'rb').read() + +rsa_signable = rsa_sign.get_signable_file('testkeys/rsa.txt') +ecdsa_signable = ecdsa_sign.get_signable_file('testkeys/ecdsa.txt') +ed25519_signable = ed25519_sign.get_signable_file('testkeys/ed25519.txt') + +# try: +# ecdsa_pub.verify(ecdsa_signable, ecdsa_sign.fields.signature.value) +# ecdsa_pub.to_file('testkeys/ecdsa.txt.sig2') +rsa_pub.verify(rsa_signable, rsa_sign.fields.signature.value) +rsa_sign.to_file('testkeys/rsa.txt.sig2') +# except: + # print("RSA validation failed") + +try: + ecdsa_pub.verify(ecdsa_signable, ecdsa_sign.fields.signature.value) + ecdsa_sign.to_file('testkeys/ecdsa.txt.sig2') +except: + print("ECDSA validation failed") + +try: + ed25519_pub.verify(ed25519_signable, ed25519_sign.fields.signature.value) + ed25519_sign.to_file('testkeys/ed25519.txt.sig2') +except: + print("Ed25519 validation failed") + + + +try: + rsasig = _S.SSHSignature(rsa_priv) + rsasig.sign(rsa_data) + rsa_pub.verify(rsasig.get_signable(rsa_data), rsasig.fields.signature.value) +except: + print("RSA validation after signing failed") + +try: + ecdsasig = _S.SSHSignature(ecdsa_priv) + ecdsasig.sign(ecdsa_data) + ecdsa_pub.verify(ecdsasig.get_signable(ecdsa_data), ecdsasig.fields.signature.value) +except: + print("ECDSA validation after signing failed") + +try: + ed25519sig = _S.SSHSignature(ed25519_priv) + ed25519sig.sign(ed25519_data) + ed25519_pub.verify(ed25519sig.get_signable(ed25519_data), ed25519sig.fields.signature.value) +except: + print("Ed25519 validation after signing failed")