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 +[](https://badge.fury.io/py/sshkey-tools) + + + + +[](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 @@ + + + + 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 @@
-def sign(self) ‑> bool
+def sign(self, **kwargs) ‑> bool
Sign the certificate
+**kwargs_EX.NotSignedExceptiondef 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:]
-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 @@@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 @@value : Union[str, bytes]encoding : str@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)
StringField:
decodeencodeencodefactoryfrom_decodeget_nameclass NonceField(StringField):
+class NonceField(BytestringField):
"""
Contains the nonce for the certificate, randomly generated
this protects the integrity of the private key, especially
@@ -5415,7 +5432,6 @@ Inherited members
Ancestors
@@ -5467,14 +5483,14 @@ Returns
Inherited members
@@ -5608,7 +5624,7 @@ Inherited members
StringField:
decode
-encode
+encode
factory
from_decode
get_name
@@ -5871,7 +5887,7 @@ Inherited members
StringField:
decode
-encode
+encode
factory
from_decode
get_name
@@ -6381,8 +6397,11 @@ Inherited members
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)
@@ -6439,7 +6458,7 @@ Inherited members
"""
return self.private_key is not None
- def sign(self, data: bytes) -> None:
+ def sign(self, data: bytes, **kwargs) -> None:
"""
Placeholder signing function
"""
@@ -6599,7 +6618,7 @@ Methods
-def sign(self, data: bytes) ‑> None
+def sign(self, data: bytes, **kwargs) ‑> None
-
Placeholder signing function
@@ -6607,7 +6626,7 @@ Methods
Expand source code
-def sign(self, data: bytes) -> None:
+def sign(self, data: bytes, **kwargs) -> None:
"""
Placeholder signing function
"""
@@ -6658,8 +6677,7 @@ Inherited members
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]:
@@ -6673,9 +6691,7 @@ Inherited members
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)
Ancestors
@@ -6685,7 +6701,6 @@ Ancestors
Subclasses
@@ -6700,12 +6715,50 @@ Class variables
+Static methods
+
+
+def encode(value: str, encoding: str = 'utf-8')
+
+-
+
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
+
+Returns
+
+bytes
+- Packed byte string containing the source data
+
+
+
+Expand source code
+
+@classmethod
+def encode(cls, value: str, encoding: str = "utf-8"):
+ """
+ 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
+
+ Returns:
+ bytes: Packed byte string containing the source data
+ """
+ return super().encode(value, encoding)
+
+
+
Inherited members
BytestringField:
-
diff --git a/docs/index.html b/docs/index.html
index 4fdfdc2..dfa4a21 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -46,12 +46,7 @@
OpenSSH Certificates
- 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
With pip
pip3 install sshkey-tools
@@ -481,6 +476,11 @@ Loading, re-cre
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
@@ -585,6 +585,7 @@ Index
- Creating, signing and verifying certificates
- Loading, re-creating and verifying existing certificates
- Changelog
+- 0.9.1
- 0.9
- 0.8.2
- 0.8.1
diff --git a/docs/keys.html b/docs/keys.html
index 93e41c9..afa1da6 100644
--- a/docs/keys.html
+++ b/docs/keys.html
@@ -55,7 +55,7 @@ Module sshkey_tools.keys
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
@@ -158,12 +158,15 @@ Module sshkey_tools.keys
_SERIALIZATION.Encoding.OpenSSH,
_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":
"""
@@ -295,7 +298,7 @@ Module sshkey_tools.keys
return " ".join(
[
ensure_string(self.serialize(), encoding),
- ensure_string(getattr(self, "comment", ""), encoding),
+ ensure_string(nullsafe_getattr(self, "comment", ""), encoding),
]
)
@@ -2893,12 +2896,15 @@ Returns
_SERIALIZATION.Encoding.OpenSSH,
_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":
"""
@@ -3030,7 +3036,7 @@ Returns
return " ".join(
[
ensure_string(self.serialize(), encoding),
- ensure_string(getattr(self, "comment", ""), encoding),
+ ensure_string(nullsafe_getattr(self, "comment", ""), encoding),
]
)
@@ -3104,7 +3110,7 @@ Returns
-def from_class(key_class: Union[cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey], comment: Union[str, bytes] = None, key_type: Union[str, bytes] = None) ‑> PublicKey
+def from_class(key_class: Union[cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey], comment: Union[str, bytes] = '', key_type: Union[str, bytes] = None) ‑> PublicKey
-
Creates a new SSH Public key from a cryptography class
@@ -3135,7 +3141,7 @@ Returns
def from_class(
cls,
key_class: PubkeyClasses,
- comment: Union[str, bytes] = None,
+ comment: Union[str, bytes] = "",
key_type: Union[str, bytes] = None,
) -> "PublicKey":
"""
@@ -3380,7 +3386,7 @@ Returns
return " ".join(
[
ensure_string(self.serialize(), encoding),
- ensure_string(getattr(self, "comment", ""), encoding),
+ ensure_string(nullsafe_getattr(self, "comment", ""), encoding),
]
)
diff --git a/docs/utils.html b/docs/utils.html
index 4be0c87..e953cf9 100644
--- a/docs/utils.html
+++ b/docs/utils.html
@@ -33,13 +33,15 @@ Module sshkey_tools.utils
import hashlib as hl
import sys
import datetime
-import pytimeparse2
+
from base64 import b64encode
from random import randint
from secrets import randbits
from typing import Dict, List, Union
from uuid import uuid4
+from pytimeparse2 import parse as time_parse
+
NoneType = type(None)
@@ -260,6 +262,22 @@ Module sshkey_tools.utils
)
+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.
@@ -283,7 +301,7 @@ Module sshkey_tools.utils
return return_dict
-def str_to_timedelta(str_delta: str) -> datetime.timedelta:
+def str_to_time_delta(str_delta: str) -> datetime.timedelta:
"""Uses the package pytimeparse2 by wroberts/onegreyonewhite
to convert a string into a timedelta object.
Examples:
@@ -328,10 +346,12 @@ Module sshkey_tools.utils
datetime.timedelta: The time delta object
"""
try:
- parsed = pytimeparse2.parse(str_delta, as_timedelta=True, raise_exception=True)
+ parsed = time_parse(str_delta, as_timedelta=True, raise_exception=True)
return parsed
- except Exception as e:
- raise ValueError(f"Could not parse time delta string {str_delta} : {e}") from e
+ except Exception as ex:
+ raise ValueError(
+ f"Could not parse time delta string {str_delta} : {ex}"
+ ) from ex
@@ -718,6 +738,42 @@ Returns
)
+
+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
+
+
+
+Expand source code
+
+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 random_keyid() ‑> str
@@ -840,8 +896,8 @@ Returns
)
-
-def str_to_timedelta(str_delta: str) ‑> datetime.timedelta
+
+def str_to_time_delta(str_delta: str) ‑> datetime.timedelta
-
Uses the package pytimeparse2 by wroberts/onegreyonewhite
@@ -894,7 +950,7 @@
Returns
Expand source code
-def str_to_timedelta(str_delta: str) -> datetime.timedelta:
+def str_to_time_delta(str_delta: str) -> datetime.timedelta:
"""Uses the package pytimeparse2 by wroberts/onegreyonewhite
to convert a string into a timedelta object.
Examples:
@@ -939,10 +995,12 @@ Returns
datetime.timedelta: The time delta object
"""
try:
- parsed = pytimeparse2.parse(str_delta, as_timedelta=True, raise_exception=True)
+ parsed = time_parse(str_delta, as_timedelta=True, raise_exception=True)
return parsed
- except Exception as e:
- raise ValueError(f"Could not parse time delta string {str_delta} : {e}") from e
+ except Exception as ex:
+ raise ValueError(
+ f"Could not parse time delta string {str_delta} : {ex}"
+ ) from ex
@@ -972,11 +1030,12 @@ Index
join_dicts
long_to_bytes
md5_fingerprint
+nullsafe_getattr
random_keyid
random_serial
sha256_fingerprint
sha512_fingerprint
-str_to_timedelta
+str_to_time_delta
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")