From 2b28f25a38246c3dde85c2b3db4b86489d59b4c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:10:04 +0000 Subject: [PATCH 1/4] build(deps): bump cryptography from 43.0.3 to 46.0.3 Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.3 to 46.0.3. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/43.0.3...46.0.3) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- poetry.lock | 261 +++++++++++++++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 154 insertions(+), 109 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0e2d4eac..96546fd1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -479,83 +479,100 @@ files = [ [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "test"] files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] [package.dependencies] -pycparser = "*" +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "chardet" @@ -809,52 +826,79 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "43.0.3" +version = "46.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["main"] files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, ] [package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -3181,6 +3225,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main", "test"] +markers = "implementation_name != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -4803,4 +4848,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "1a3968dbde8f4356b4d93b17f5bcf75f2bc38587553273742de05d9f0f6ee87c" +content-hash = "30602d4dd2724dce8a25d005dec3043dc9abd25f3a9b6daf7a708185efd6c964" diff --git a/pyproject.toml b/pyproject.toml index 15edb00c..7bd18839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ openshift-client = "1.0.18" rich = ">=12.5,<14.0" ray = {version = "2.47.1", extras = ["data", "default"]} kubernetes = ">= 27.2.0" -cryptography = "43.0.3" +cryptography = "46.0.3" executing = "1.2.0" pydantic = ">= 2.10.6" ipywidgets = "8.1.2" From bac0fd27bf97388784ec565dcf427e7e0b4a694c Mon Sep 17 00:00:00 2001 From: Laura Fitzgerald Date: Wed, 26 Nov 2025 11:25:44 +0000 Subject: [PATCH 2/4] additional security changes for cert generation, automatically lifecyle certs for clusters to remove manual step --- .../additional-demos/hf_interactive.ipynb | 13 - .../additional-demos/local_interactive.ipynb | 13 - .../guided-demos/2_basic_interactive.ipynb | 13 - .../2_basic_interactive.ipynb | 13 - .../preview_nbs/2_basic_interactive.ipynb | 13 - .../common/utils/generate_cert.py | 442 ++++++++++++++++-- src/codeflare_sdk/ray/cluster/cluster.py | 60 ++- src/codeflare_sdk/ray/cluster/config.py | 4 + 8 files changed, 459 insertions(+), 112 deletions(-) diff --git a/demo-notebooks/additional-demos/hf_interactive.ipynb b/demo-notebooks/additional-demos/hf_interactive.ipynb index 9b32ab2e..009c3ba5 100644 --- a/demo-notebooks/additional-demos/hf_interactive.ipynb +++ b/demo-notebooks/additional-demos/hf_interactive.ipynb @@ -192,19 +192,6 @@ "Now we can connect directly to our Ray cluster via the Ray python client:" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "60276d86", - "metadata": {}, - "outputs": [], - "source": [ - "from codeflare_sdk import generate_cert\n", - "# Create required TLS cert and export the environment variables to enable TLS\n", - "generate_cert.generate_tls_cert(cluster_name, cluster.config.namespace)\n", - "generate_cert.export_env(cluster_name, cluster.config.namespace)" - ] - }, { "cell_type": "markdown", "id": "44dba6a0-8275-4726-8911-6b6ec467b6a3", diff --git a/demo-notebooks/additional-demos/local_interactive.ipynb b/demo-notebooks/additional-demos/local_interactive.ipynb index 257c6c1b..674fe828 100644 --- a/demo-notebooks/additional-demos/local_interactive.ipynb +++ b/demo-notebooks/additional-demos/local_interactive.ipynb @@ -103,19 +103,6 @@ "### Connect via the rayclient route" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf1b749e-2335-42c2-b673-26768ec9895d", - "metadata": {}, - "outputs": [], - "source": [ - "from codeflare_sdk import generate_cert\n", - "\n", - "generate_cert.generate_tls_cert(cluster_name, cluster.config.namespace)\n", - "generate_cert.export_env(cluster_name, cluster.config.namespace)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/demo-notebooks/guided-demos/2_basic_interactive.ipynb b/demo-notebooks/guided-demos/2_basic_interactive.ipynb index 683ec236..ab020ea2 100644 --- a/demo-notebooks/guided-demos/2_basic_interactive.ipynb +++ b/demo-notebooks/guided-demos/2_basic_interactive.ipynb @@ -142,19 +142,6 @@ "Now we can connect directly to our Ray cluster via the Ray python client:" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "c9436436", - "metadata": {}, - "outputs": [], - "source": [ - "from codeflare_sdk import generate_cert\n", - "# Create required TLS cert and export the environment variables to enable TLS\n", - "generate_cert.generate_tls_cert(cluster_name, cluster.config.namespace)\n", - "generate_cert.export_env(cluster_name, cluster.config.namespace)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb b/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb index 9c816c53..2b50a1ab 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb @@ -130,19 +130,6 @@ "Now we can connect directly to our Ray cluster via the Ray python client:" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "13eb52f6", - "metadata": {}, - "outputs": [], - "source": [ - "from codeflare_sdk import generate_cert\n", - "# Create required TLS cert and export the environment variables to enable TLS\n", - "generate_cert.generate_tls_cert(cluster_name, cluster.config.namespace)\n", - "generate_cert.export_env(cluster_name, cluster.config.namespace)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb b/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb index 1de3fc9c..f07a5040 100644 --- a/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb +++ b/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb @@ -130,19 +130,6 @@ "Now we can connect directly to our Ray cluster via the Ray python client:" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "e5308271", - "metadata": {}, - "outputs": [], - "source": [ - "from codeflare_sdk import generate_cert\n", - "# Create required TLS cert and export the environment variables to enable TLS\n", - "generate_cert.generate_tls_cert(cluster_name, cluster.config.namespace)\n", - "generate_cert.export_env(cluster_name, cluster.config.namespace)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/src/codeflare_sdk/common/utils/generate_cert.py b/src/codeflare_sdk/common/utils/generate_cert.py index 8e7c7af6..cda35031 100644 --- a/src/codeflare_sdk/common/utils/generate_cert.py +++ b/src/codeflare_sdk/common/utils/generate_cert.py @@ -14,13 +14,15 @@ import base64 import os +import stat +import ipaddress +from pathlib import Path from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.backends import default_backend from cryptography import x509 -from cryptography.x509.oid import NameOID -import ipaddress +from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID import datetime +from datetime import timezone from ..kubernetes_cluster.auth import ( config_check, get_api_client, @@ -33,8 +35,13 @@ def generate_ca_cert(days: int = 30): """ Generates a self-signed CA certificate and private key, encoded in base64 format. + The certificate includes RFC 5280 compliant extensions: + - BasicConstraints (CA:TRUE) + - KeyUsage (keyCertSign, cRLSign) + - SubjectKeyIdentifier + Similar to: - openssl req -x509 -nodes -newkey rsa:2048 -keyout ca.key -days 1826 -out ca.crt -subj '/CN=root-ca' + openssl req -x509 -nodes -newkey rsa:3072 -keyout ca.key -days 1826 -out ca.crt -subj '/CN=root-ca' Args: days (int): @@ -47,7 +54,7 @@ def generate_ca_cert(days: int = 30): private_key = rsa.generate_private_key( public_exponent=65537, - key_size=2048, + key_size=3072, # Increased from 2048 for better security ) key = base64.b64encode( @@ -77,10 +84,33 @@ def generate_ca_cert(days: int = 30): ] ) ) - .not_valid_before(datetime.datetime.today() - one_day) - .not_valid_after(datetime.datetime.today() + (one_day * days)) + .not_valid_before(datetime.datetime.now(timezone.utc) - one_day) + .not_valid_after(datetime.datetime.now(timezone.utc) + (one_day * days)) .serial_number(x509.random_serial_number()) .public_key(public_key) + # Add CA certificate extensions for RFC 5280 compliance + .add_extension( + x509.BasicConstraints(ca=True, path_length=0), + critical=True, + ) + .add_extension( + x509.KeyUsage( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(public_key), + critical=False, + ) ) certificate = base64.b64encode( builder.sign(private_key=private_key, algorithm=hashes.SHA256()).public_bytes( @@ -127,10 +157,24 @@ def get_secret_name(cluster_name, namespace, api_instance): return _kube_api_error_handling(e) -def generate_tls_cert(cluster_name, namespace, days=30): +def generate_tls_cert(cluster_name, namespace, days=30, force_regenerate=False): """ Generates a TLS certificate and key for a Ray cluster, saving them locally along with the CA certificate. + The certificate includes RFC 5280 compliant extensions: + - BasicConstraints (CA:FALSE) + - KeyUsage (digitalSignature, keyEncipherment) + - ExtendedKeyUsage (serverAuth, clientAuth for mTLS) + - SubjectAlternativeName (localhost, 127.0.0.1, ::1) + - SubjectKeyIdentifier + - AuthorityKeyIdentifier + + Files are created with restricted permissions (0600) for security. + + Certificates are stored in a user-private directory: + - Default: ~/.local/share/codeflare/tls/{cluster_name}-{namespace}/ + - Override via CODEFLARE_TLS_DIR environment variable + Args: cluster_name (str): The name of the Ray cluster. @@ -138,19 +182,42 @@ def generate_tls_cert(cluster_name, namespace, days=30): The Kubernetes namespace where the Ray cluster is located. days (int): The number of days for which the TLS certificate will be valid. Default is 30. + force_regenerate (bool): + If True, regenerates certificates even if they already exist. Useful when the server + CA secret has been rotated and existing certificates are no longer valid. Default is False. Files Created: - - ca.crt: The CA certificate. - - tls.crt: The TLS certificate signed by the CA. - - tls.key: The private key for the TLS certificate. + - ca.crt: The CA certificate (permissions: 0600). + - tls.crt: The TLS certificate signed by the CA (permissions: 0600). + - tls.key: The private key for the TLS certificate (permissions: 0600). Raises: Exception: If an error occurs while retrieving the CA secret. + + Example: + # Normal generation + generate_tls_cert("my-cluster", "default") + + # Force regeneration if CA was rotated + generate_tls_cert("my-cluster", "default", force_regenerate=True) """ - tls_dir = os.path.join(os.getcwd(), f"tls-{cluster_name}-{namespace}") - if not os.path.exists(tls_dir): - os.makedirs(tls_dir) + tls_base_dir = _get_tls_base_dir() + tls_dir = tls_base_dir / f"{cluster_name}-{namespace}" + + # Check if certificates already exist and skip if not forcing regeneration + if not force_regenerate and tls_dir.exists(): + ca_crt = tls_dir / "ca.crt" + tls_crt = tls_dir / "tls.crt" + tls_key = tls_dir / "tls.key" + + if ca_crt.exists() and tls_crt.exists() and tls_key.exists(): + # Certificates already exist, no need to regenerate + return + + # Create directory with secure permissions (including parent directories) + tls_dir.mkdir(mode=0o700, parents=True, exist_ok=True) + tls_dir = str(tls_dir) # Similar to: # oc get secret ca-secret- -o template='{{index .data "tls.key"}}' @@ -176,32 +243,36 @@ def generate_tls_cert(cluster_name, namespace, days=30): f"Available keys: {list(secret.keys())}" ) - # Decode and write CA certificate - ca_cert_pem = base64.b64decode(ca_cert).decode("utf-8") - with open(os.path.join(tls_dir, "ca.crt"), "w") as f: - f.write(ca_cert_pem) + # Load the CA certificate for issuer information and AuthorityKeyIdentifier + ca_cert_obj = x509.load_pem_x509_certificate(base64.b64decode(ca_cert)) + + ca_private_key = serialization.load_pem_private_key(base64.b64decode(ca_key), None) + + # Write CA certificate with secure permissions + ca_crt_path = os.path.join(tls_dir, "ca.crt") + with open(ca_crt_path, "w") as f: + f.write(base64.b64decode(ca_cert).decode("utf-8")) + os.chmod(ca_crt_path, stat.S_IRUSR | stat.S_IWUSR) # Set permissions to 0600 - # Extract CA subject to use as issuer for client certificate - ca_cert_obj = x509.load_pem_x509_certificate( - ca_cert_pem.encode("utf-8"), default_backend() - ) - ca_subject = ca_cert_obj.subject # Generate tls.key and signed tls.cert locally for ray client # Similar to running these commands: - # openssl req -nodes -newkey rsa:2048 -keyout ${TLSDIR}/tls.key -out ${TLSDIR}/tls.csr -subj '/CN=local' + # openssl req -nodes -newkey rsa:3072 -keyout ${TLSDIR}/tls.key -out ${TLSDIR}/tls.csr -subj '/CN=local' # cat <${TLSDIR}/domain.ext # authorityKeyIdentifier=keyid,issuer # basicConstraints=CA:FALSE + # keyUsage = digitalSignature, keyEncipherment + # extendedKeyUsage = serverAuth, clientAuth # subjectAltName = @alt_names # [alt_names] - # DNS.1 = 127.0.0.1 - # DNS.2 = localhost + # DNS.1 = localhost + # IP.1 = 127.0.0.1 + # IP.2 = ::1 # EOF # openssl x509 -req -CA ${TLSDIR}/ca.crt -CAkey ${TLSDIR}/ca.key -in ${TLSDIR}/tls.csr -out ${TLSDIR}/tls.crt -days 365 -CAcreateserial -extfile ${TLSDIR}/domain.ext key = rsa.generate_private_key( public_exponent=65537, - key_size=2048, + key_size=3072, # Increased from 2048 for better security ) tls_key = key.private_bytes( @@ -209,25 +280,20 @@ def generate_tls_cert(cluster_name, namespace, days=30): serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) - with open(os.path.join(tls_dir, "tls.key"), "w") as f: + tls_key_path = os.path.join(tls_dir, "tls.key") + with open(tls_key_path, "w") as f: f.write(tls_key.decode("utf-8")) + os.chmod(tls_key_path, stat.S_IRUSR | stat.S_IWUSR) # Set permissions to 0600 + # Build SAN list with service DNS names for cluster communication head_svc_name = f"{cluster_name}-head-svc" service_dns = f"{head_svc_name}.{namespace}.svc" service_dns_cluster_local = f"{head_svc_name}.{namespace}.svc.cluster.local" - san_list = [ - x509.DNSName("localhost"), - x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), - x509.DNSName(head_svc_name), - x509.DNSName(service_dns), - x509.DNSName(service_dns_cluster_local), - ] - one_day = datetime.timedelta(1, 0, 0) tls_cert = ( x509.CertificateBuilder() - .issuer_name(ca_subject) + .issuer_name(ca_cert_obj.subject) .subject_name( x509.Name( [ @@ -236,21 +302,72 @@ def generate_tls_cert(cluster_name, namespace, days=30): ) ) .public_key(key.public_key()) - .not_valid_before(datetime.datetime.today() - one_day) - .not_valid_after(datetime.datetime.today() + (one_day * days)) + .not_valid_before(datetime.datetime.now(timezone.utc) - one_day) + .not_valid_after(datetime.datetime.now(timezone.utc) + (one_day * days)) .serial_number(x509.random_serial_number()) + # Add TLS certificate extensions for RFC 5280 compliance + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ) .add_extension( - x509.SubjectAlternativeName(san_list), - False, + x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=True, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.SERVER_AUTH, + ExtendedKeyUsageOID.CLIENT_AUTH, # For mTLS support + ]), + critical=True, + ) + .add_extension( + x509.SubjectAlternativeName([ + x509.DNSName("localhost"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + x509.IPAddress(ipaddress.IPv6Address("::1")), + x509.DNSName(head_svc_name), + x509.DNSName(service_dns), + x509.DNSName(service_dns_cluster_local), + ]), + critical=False, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(key.public_key()), + critical=False, + ) + .add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key( + ca_cert_obj.public_key() + ), + critical=False, ) .sign( - serialization.load_pem_private_key(base64.b64decode(ca_key), None), + ca_private_key, hashes.SHA256(), ) ) - with open(os.path.join(tls_dir, "tls.crt"), "w") as f: + tls_crt_path = os.path.join(tls_dir, "tls.crt") + with open(tls_crt_path, "w") as f: f.write(tls_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")) + os.chmod(tls_crt_path, stat.S_IRUSR | stat.S_IWUSR) # Set permissions to 0600 + + del ca_key, ca_private_key + try: + del secret + except: + pass # May already be out of scope def export_env(cluster_name, namespace): @@ -269,8 +386,245 @@ def export_env(cluster_name, namespace): - RAY_TLS_SERVER_KEY: Path to the TLS server private key. - RAY_TLS_CA_CERT: Path to the CA certificate. """ - tls_dir = os.path.join(os.getcwd(), f"tls-{cluster_name}-{namespace}") + tls_base_dir = _get_tls_base_dir() + tls_dir = tls_base_dir / f"{cluster_name}-{namespace}" + tls_dir = str(tls_dir) os.environ["RAY_USE_TLS"] = "1" os.environ["RAY_TLS_SERVER_CERT"] = os.path.join(tls_dir, "tls.crt") os.environ["RAY_TLS_SERVER_KEY"] = os.path.join(tls_dir, "tls.key") os.environ["RAY_TLS_CA_CERT"] = os.path.join(tls_dir, "ca.crt") + + +def cleanup_tls_cert(cluster_name, namespace): + """ + Removes TLS certificates and keys for a specific Ray cluster. + + This should be called when a cluster is deleted to clean up sensitive key material. + + Args: + cluster_name (str): + The name of the Ray cluster. + namespace (str): + The Kubernetes namespace where the Ray cluster is located. + + Returns: + bool: True if certificates were removed, False if they didn't exist. + + Example: + >>> cleanup_tls_cert("my-cluster", "default") + True + """ + import shutil + + tls_base_dir = _get_tls_base_dir() + tls_dir = tls_base_dir / f"{cluster_name}-{namespace}" + + if tls_dir.exists(): + shutil.rmtree(tls_dir) + return True + return False + + +def list_tls_certificates(): + """ + Lists all TLS certificate directories and their details. + + Returns: + list: List of dictionaries containing certificate information: + - cluster_name: Name of the cluster + - namespace: Kubernetes namespace + - path: Full path to certificate directory + - created: Creation time of the directory + - size: Total size of certificates in bytes + - cert_expiry: Expiration date of tls.crt (if readable) + + Example: + >>> certs = list_tls_certificates() + >>> for cert in certs: + ... print(f"{cert['cluster_name']}/{cert['namespace']}: expires {cert['cert_expiry']}") + """ + tls_base_dir = _get_tls_base_dir() + + if not tls_base_dir.exists(): + return [] + + certificates = [] + + for cert_dir in tls_base_dir.iterdir(): + if cert_dir.is_dir(): + # Parse cluster name and namespace from directory name + dir_name = cert_dir.name + parts = dir_name.rsplit("-", 1) + + if len(parts) == 2: + cluster_name, namespace = parts + else: + cluster_name = dir_name + namespace = "unknown" + + # Get directory stats + stat_info = cert_dir.stat() + created = datetime.datetime.fromtimestamp(stat_info.st_ctime) + + # Calculate total size + total_size = sum(f.stat().st_size for f in cert_dir.rglob('*') if f.is_file()) + + # Try to read certificate expiry + cert_expiry = None + tls_cert_path = cert_dir / "tls.crt" + if tls_cert_path.exists(): + try: + with open(tls_cert_path, 'rb') as f: + cert = x509.load_pem_x509_certificate(f.read()) + cert_expiry = cert.not_valid_after_utc + except Exception: + cert_expiry = None + + certificates.append({ + 'cluster_name': cluster_name, + 'namespace': namespace, + 'path': str(cert_dir), + 'created': created, + 'size': total_size, + 'cert_expiry': cert_expiry, + }) + + return certificates + + +def cleanup_expired_certificates(dry_run=True): + """ + Removes TLS certificates that have expired. + + Args: + dry_run (bool): + If True (default), only lists expired certificates without deleting them. + Set to False to actually delete expired certificates. + + Returns: + list: List of certificate paths that were (or would be) removed. + + Example: + >>> # Check what would be deleted + >>> expired = cleanup_expired_certificates(dry_run=True) + >>> print(f"Found {len(expired)} expired certificates") + >>> + >>> # Actually delete them + >>> cleanup_expired_certificates(dry_run=False) + """ + import shutil + + now = datetime.datetime.now(timezone.utc) + expired_certs = [] + + certificates = list_tls_certificates() + + for cert_info in certificates: + if cert_info['cert_expiry'] and cert_info['cert_expiry'] < now: + expired_certs.append(cert_info['path']) + + if not dry_run: + cert_dir = Path(cert_info['path']) + if cert_dir.exists(): + shutil.rmtree(cert_dir) + + return expired_certs + + +def cleanup_old_certificates(days=30, dry_run=True): + """ + Removes TLS certificates older than a specified number of days. + + Args: + days (int): + Remove certificates created more than this many days ago. Default is 30. + dry_run (bool): + If True (default), only lists old certificates without deleting them. + Set to False to actually delete old certificates. + + Returns: + list: List of certificate paths that were (or would be) removed. + + Example: + >>> # Check certificates older than 90 days + >>> old = cleanup_old_certificates(days=90, dry_run=True) + >>> print(f"Found {len(old)} certificates older than 90 days") + >>> + >>> # Delete certificates older than 30 days + >>> cleanup_old_certificates(days=30, dry_run=False) + """ + import shutil + + cutoff_date = datetime.datetime.now() - datetime.timedelta(days=days) + old_certs = [] + + certificates = list_tls_certificates() + + for cert_info in certificates: + if cert_info['created'] < cutoff_date: + old_certs.append(cert_info['path']) + + if not dry_run: + cert_dir = Path(cert_info['path']) + if cert_dir.exists(): + shutil.rmtree(cert_dir) + + return old_certs + + +def refresh_tls_cert(cluster_name, namespace, days=30): + """ + Refreshes TLS certificates by removing old ones and generating new ones. + + This is useful when the server CA secret has been rotated and existing + client certificates are no longer valid. + + Args: + cluster_name (str): + The name of the Ray cluster. + namespace (str): + The Kubernetes namespace where the Ray cluster is located. + days (int): + The number of days for which the new TLS certificate will be valid. Default is 30. + + Returns: + bool: True if certificates were successfully refreshed. + + Example: + >>> # Server CA was rotated, refresh client certificates + >>> refresh_tls_cert("my-cluster", "default") + >>> export_env("my-cluster", "default") + >>> # Now you can reconnect with fresh certificates + """ + # Remove old certificates + cleanup_tls_cert(cluster_name, namespace) + + # Generate new ones + generate_tls_cert(cluster_name, namespace, days=days, force_regenerate=True) + + return True + +def _get_tls_base_dir(): + """ + Get the base directory for TLS certificate storage. + + Priority order: + 1. CODEFLARE_TLS_DIR environment variable + 2. XDG_DATA_HOME/codeflare/tls (Linux standard) + 3. ~/.local/share/codeflare/tls (fallback) + + Returns: + Path: Base directory for TLS certificates + """ + # Check for explicit override + tls_dir_env = os.environ.get('CODEFLARE_TLS_DIR') + if tls_dir_env: + return Path(tls_dir_env) + + # Use XDG Base Directory specification + xdg_data_home = os.environ.get('XDG_DATA_HOME') + if xdg_data_home: + return Path(xdg_data_home) / "codeflare" / "tls" + + # Fallback to standard location + return Path.home() / ".local" / "share" / "codeflare" / "tls" \ No newline at end of file diff --git a/src/codeflare_sdk/ray/cluster/cluster.py b/src/codeflare_sdk/ray/cluster/cluster.py index 7167ea1a..0d756f62 100644 --- a/src/codeflare_sdk/ray/cluster/cluster.py +++ b/src/codeflare_sdk/ray/cluster/cluster.py @@ -22,9 +22,7 @@ from typing import List, Optional, Tuple, Dict import copy -from ray.job_submission import JobSubmissionClient, JobStatus -import time -import uuid +from ray.job_submission import JobSubmissionClient import warnings from ...common.utils import get_current_namespace @@ -285,6 +283,9 @@ def down(self): """ Deletes the AppWrapper yaml, scaling-down and deleting all resources associated with the cluster. + + If cleanup_tls_certs is True (default), also removes the TLS certificates + generated for this cluster to prevent accumulation of sensitive key material. """ namespace = self.config.namespace resource_name = self.config.name @@ -306,6 +307,13 @@ def down(self): print( f"Ray Cluster: '{self.config.name}' has successfully been deleted" ) + + # Automatically clean up TLS certificates if enabled + if self.config.cleanup_tls_certs: + from codeflare_sdk.common.utils import generate_cert + if generate_cert.cleanup_tls_cert(resource_name, namespace): + print(f"TLS certificates for '{resource_name}' have been removed") + except Exception as e: # pragma: no cover return _kube_api_error_handling(e) @@ -434,6 +442,9 @@ def wait_ready(self, timeout: Optional[int] = None, dashboard_check: bool = True ready or the timeout is reached. If dashboard_check is enabled, it will also check for the readiness of the dashboard. + TLS certificates are automatically generated and environment variables are set + when the cluster becomes ready. This is required for mTLS connections to the cluster. + Args: timeout (Optional[int]): The maximum time to wait for the cluster to be ready in seconds. If None, waits indefinitely. @@ -463,6 +474,17 @@ def wait_ready(self, timeout: Optional[int] = None, dashboard_check: bool = True time += 5 print("Requested cluster is up and running!") + # Automatically generate TLS certificates (required for mTLS) + try: + from codeflare_sdk.common.utils import generate_cert + generate_cert.generate_tls_cert(self.config.name, self.config.namespace) + generate_cert.export_env(self.config.name, self.config.namespace) + print(f"TLS certificates generated for '{self.config.name}'") + except Exception as e: + # Don't fail cluster setup if certificate generation fails + print(f"Warning: Could not generate TLS certificates: {e}") + print("You can manually generate certificates using generate_cert.generate_tls_cert()") + while dashboard_check: if timeout and time >= timeout: raise TimeoutError( @@ -500,6 +522,38 @@ def cluster_uri(self) -> str: Returns a string containing the cluster's URI. """ return f"ray://{self.config.name}-head-svc.{self.config.namespace}.svc:10001" + + def refresh_certificates(self): + """ + Refreshes TLS certificates by removing old ones and generating new ones. + + This is useful when: + - The server CA secret has been rotated + - Certificates have expired + - You encounter TLS handshake failures + - You need to regenerate certificates for any reason + + The method will: + 1. Remove existing client certificates + 2. Fetch the latest CA from Kubernetes + 3. Generate new client certificates + 4. Update environment variables for Ray + + Example: + >>> # If you get TLS errors after CA rotation + >>> cluster.refresh_certificates() + >>> # Now you can reconnect + >>> ray.init(address=cluster.cluster_uri()) + """ + from codeflare_sdk.common.utils import generate_cert + + print(f"Refreshing TLS certificates for '{self.config.name}'...") + + # Use the refresh function which handles cleanup and regeneration + generate_cert.refresh_tls_cert(self.config.name, self.config.namespace) + generate_cert.export_env(self.config.name, self.config.namespace) + + print(f"✓ TLS certificates refreshed for '{self.config.name}'") def cluster_dashboard_uri(self) -> str: """ diff --git a/src/codeflare_sdk/ray/cluster/config.py b/src/codeflare_sdk/ray/cluster/config.py index 0715070f..81d1c7b2 100644 --- a/src/codeflare_sdk/ray/cluster/config.py +++ b/src/codeflare_sdk/ray/cluster/config.py @@ -94,6 +94,9 @@ class ClusterConfiguration: Kubernetes secret reference containing Redis password. ex: {"name": "secret-name", "key": "password-key"} external_storage_namespace: The storage namespace to use for GCS fault tolerance. By default, KubeRay sets it to the UID of RayCluster. + cleanup_tls_certs: + A boolean indicating whether to automatically clean up TLS certificates when the cluster is deleted. + Default is True for automatic cleanup. Can be set to False to retain certificates for debugging or audit purposes. """ name: str @@ -133,6 +136,7 @@ class ClusterConfiguration: redis_address: Optional[str] = None redis_password_secret: Optional[Dict[str, str]] = None external_storage_namespace: Optional[str] = None + cleanup_tls_certs: bool = True def __post_init__(self): if not self.verify_tls: From 5eacedf717f7a3ab942d6418382ee9f901c5c2ea Mon Sep 17 00:00:00 2001 From: Laura Fitzgerald Date: Wed, 26 Nov 2025 11:56:14 +0000 Subject: [PATCH 3/4] lint and test fixes --- .../common/utils/generate_cert.py | 118 ++++++------ .../common/utils/test_generate_cert.py | 180 +++++++++++++++--- .../ray/cluster/build_ray_cluster.py | 48 ++--- src/codeflare_sdk/ray/cluster/cluster.py | 20 +- src/codeflare_sdk/ray/cluster/test_cluster.py | 32 +++- src/codeflare_sdk/ray/rayjobs/rayjob.py | 6 +- 6 files changed, 280 insertions(+), 124 deletions(-) diff --git a/src/codeflare_sdk/common/utils/generate_cert.py b/src/codeflare_sdk/common/utils/generate_cert.py index cda35031..6913f147 100644 --- a/src/codeflare_sdk/common/utils/generate_cert.py +++ b/src/codeflare_sdk/common/utils/generate_cert.py @@ -170,7 +170,7 @@ def generate_tls_cert(cluster_name, namespace, days=30, force_regenerate=False): - AuthorityKeyIdentifier Files are created with restricted permissions (0600) for security. - + Certificates are stored in a user-private directory: - Default: ~/.local/share/codeflare/tls/{cluster_name}-{namespace}/ - Override via CODEFLARE_TLS_DIR environment variable @@ -194,27 +194,27 @@ def generate_tls_cert(cluster_name, namespace, days=30, force_regenerate=False): Raises: Exception: If an error occurs while retrieving the CA secret. - + Example: # Normal generation generate_tls_cert("my-cluster", "default") - + # Force regeneration if CA was rotated generate_tls_cert("my-cluster", "default", force_regenerate=True) """ tls_base_dir = _get_tls_base_dir() tls_dir = tls_base_dir / f"{cluster_name}-{namespace}" - + # Check if certificates already exist and skip if not forcing regeneration if not force_regenerate and tls_dir.exists(): ca_crt = tls_dir / "ca.crt" tls_crt = tls_dir / "tls.crt" tls_key = tls_dir / "tls.key" - + if ca_crt.exists() and tls_crt.exists() and tls_key.exists(): # Certificates already exist, no need to regenerate return - + # Create directory with secure permissions (including parent directories) tls_dir.mkdir(mode=0o700, parents=True, exist_ok=True) tls_dir = str(tls_dir) @@ -254,7 +254,6 @@ def generate_tls_cert(cluster_name, namespace, days=30, force_regenerate=False): f.write(base64.b64decode(ca_cert).decode("utf-8")) os.chmod(ca_crt_path, stat.S_IRUSR | stat.S_IWUSR) # Set permissions to 0600 - # Generate tls.key and signed tls.cert locally for ray client # Similar to running these commands: # openssl req -nodes -newkey rsa:3072 -keyout ${TLSDIR}/tls.key -out ${TLSDIR}/tls.csr -subj '/CN=local' @@ -325,21 +324,25 @@ def generate_tls_cert(cluster_name, namespace, days=30, force_regenerate=False): critical=True, ) .add_extension( - x509.ExtendedKeyUsage([ - ExtendedKeyUsageOID.SERVER_AUTH, - ExtendedKeyUsageOID.CLIENT_AUTH, # For mTLS support - ]), + x509.ExtendedKeyUsage( + [ + ExtendedKeyUsageOID.SERVER_AUTH, + ExtendedKeyUsageOID.CLIENT_AUTH, # For mTLS support + ] + ), critical=True, ) .add_extension( - x509.SubjectAlternativeName([ - x509.DNSName("localhost"), - x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), - x509.IPAddress(ipaddress.IPv6Address("::1")), - x509.DNSName(head_svc_name), - x509.DNSName(service_dns), - x509.DNSName(service_dns_cluster_local), - ]), + x509.SubjectAlternativeName( + [ + x509.DNSName("localhost"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + x509.IPAddress(ipaddress.IPv6Address("::1")), + x509.DNSName(head_svc_name), + x509.DNSName(service_dns), + x509.DNSName(service_dns_cluster_local), + ] + ), critical=False, ) .add_extension( @@ -362,7 +365,7 @@ def generate_tls_cert(cluster_name, namespace, days=30, force_regenerate=False): with open(tls_crt_path, "w") as f: f.write(tls_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")) os.chmod(tls_crt_path, stat.S_IRUSR | stat.S_IWUSR) # Set permissions to 0600 - + del ca_key, ca_private_key try: del secret @@ -467,27 +470,31 @@ def list_tls_certificates(): created = datetime.datetime.fromtimestamp(stat_info.st_ctime) # Calculate total size - total_size = sum(f.stat().st_size for f in cert_dir.rglob('*') if f.is_file()) + total_size = sum( + f.stat().st_size for f in cert_dir.rglob("*") if f.is_file() + ) # Try to read certificate expiry cert_expiry = None tls_cert_path = cert_dir / "tls.crt" if tls_cert_path.exists(): try: - with open(tls_cert_path, 'rb') as f: + with open(tls_cert_path, "rb") as f: cert = x509.load_pem_x509_certificate(f.read()) cert_expiry = cert.not_valid_after_utc except Exception: cert_expiry = None - certificates.append({ - 'cluster_name': cluster_name, - 'namespace': namespace, - 'path': str(cert_dir), - 'created': created, - 'size': total_size, - 'cert_expiry': cert_expiry, - }) + certificates.append( + { + "cluster_name": cluster_name, + "namespace": namespace, + "path": str(cert_dir), + "created": created, + "size": total_size, + "cert_expiry": cert_expiry, + } + ) return certificates @@ -508,7 +515,7 @@ def cleanup_expired_certificates(dry_run=True): >>> # Check what would be deleted >>> expired = cleanup_expired_certificates(dry_run=True) >>> print(f"Found {len(expired)} expired certificates") - >>> + >>> >>> # Actually delete them >>> cleanup_expired_certificates(dry_run=False) """ @@ -520,11 +527,11 @@ def cleanup_expired_certificates(dry_run=True): certificates = list_tls_certificates() for cert_info in certificates: - if cert_info['cert_expiry'] and cert_info['cert_expiry'] < now: - expired_certs.append(cert_info['path']) + if cert_info["cert_expiry"] and cert_info["cert_expiry"] < now: + expired_certs.append(cert_info["path"]) if not dry_run: - cert_dir = Path(cert_info['path']) + cert_dir = Path(cert_info["path"]) if cert_dir.exists(): shutil.rmtree(cert_dir) @@ -534,51 +541,51 @@ def cleanup_expired_certificates(dry_run=True): def cleanup_old_certificates(days=30, dry_run=True): """ Removes TLS certificates older than a specified number of days. - + Args: days (int): Remove certificates created more than this many days ago. Default is 30. dry_run (bool): If True (default), only lists old certificates without deleting them. Set to False to actually delete old certificates. - + Returns: list: List of certificate paths that were (or would be) removed. - + Example: >>> # Check certificates older than 90 days >>> old = cleanup_old_certificates(days=90, dry_run=True) >>> print(f"Found {len(old)} certificates older than 90 days") - >>> + >>> >>> # Delete certificates older than 30 days >>> cleanup_old_certificates(days=30, dry_run=False) """ import shutil - + cutoff_date = datetime.datetime.now() - datetime.timedelta(days=days) old_certs = [] - + certificates = list_tls_certificates() - + for cert_info in certificates: - if cert_info['created'] < cutoff_date: - old_certs.append(cert_info['path']) - + if cert_info["created"] < cutoff_date: + old_certs.append(cert_info["path"]) + if not dry_run: - cert_dir = Path(cert_info['path']) + cert_dir = Path(cert_info["path"]) if cert_dir.exists(): shutil.rmtree(cert_dir) - + return old_certs def refresh_tls_cert(cluster_name, namespace, days=30): """ Refreshes TLS certificates by removing old ones and generating new ones. - + This is useful when the server CA secret has been rotated and existing client certificates are no longer valid. - + Args: cluster_name (str): The name of the Ray cluster. @@ -586,10 +593,10 @@ def refresh_tls_cert(cluster_name, namespace, days=30): The Kubernetes namespace where the Ray cluster is located. days (int): The number of days for which the new TLS certificate will be valid. Default is 30. - + Returns: bool: True if certificates were successfully refreshed. - + Example: >>> # Server CA was rotated, refresh client certificates >>> refresh_tls_cert("my-cluster", "default") @@ -598,12 +605,13 @@ def refresh_tls_cert(cluster_name, namespace, days=30): """ # Remove old certificates cleanup_tls_cert(cluster_name, namespace) - + # Generate new ones generate_tls_cert(cluster_name, namespace, days=days, force_regenerate=True) - + return True + def _get_tls_base_dir(): """ Get the base directory for TLS certificate storage. @@ -617,14 +625,14 @@ def _get_tls_base_dir(): Path: Base directory for TLS certificates """ # Check for explicit override - tls_dir_env = os.environ.get('CODEFLARE_TLS_DIR') + tls_dir_env = os.environ.get("CODEFLARE_TLS_DIR") if tls_dir_env: return Path(tls_dir_env) # Use XDG Base Directory specification - xdg_data_home = os.environ.get('XDG_DATA_HOME') + xdg_data_home = os.environ.get("XDG_DATA_HOME") if xdg_data_home: return Path(xdg_data_home) / "codeflare" / "tls" # Fallback to standard location - return Path.home() / ".local" / "share" / "codeflare" / "tls" \ No newline at end of file + return Path.home() / ".local" / "share" / "codeflare" / "tls" diff --git a/src/codeflare_sdk/common/utils/test_generate_cert.py b/src/codeflare_sdk/common/utils/test_generate_cert.py index e821c48b..c1ef83c0 100644 --- a/src/codeflare_sdk/common/utils/test_generate_cert.py +++ b/src/codeflare_sdk/common/utils/test_generate_cert.py @@ -21,10 +21,12 @@ ) from cryptography.x509 import load_pem_x509_certificate import os +from pathlib import Path from codeflare_sdk.common.utils.generate_cert import ( export_env, generate_ca_cert, generate_tls_cert, + _get_tls_base_dir, ) from kubernetes import client @@ -72,17 +74,23 @@ def test_generate_tls_cert(mocker): "kubernetes.client.CoreV1Api.read_namespaced_secret", side_effect=secret_ca_retreival, ) + # Mock _get_tls_base_dir to return CWD for test isolation + mocker.patch( + "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", + return_value=Path(os.getcwd()), + ) generate_tls_cert("cluster", "namespace") - assert os.path.exists("tls-cluster-namespace") - assert os.path.exists(os.path.join("tls-cluster-namespace", "ca.crt")) - assert os.path.exists(os.path.join("tls-cluster-namespace", "tls.crt")) - assert os.path.exists(os.path.join("tls-cluster-namespace", "tls.key")) + tls_dir = os.path.join(os.getcwd(), "cluster-namespace") + assert os.path.exists(tls_dir) + assert os.path.exists(os.path.join(tls_dir, "ca.crt")) + assert os.path.exists(os.path.join(tls_dir, "tls.crt")) + assert os.path.exists(os.path.join(tls_dir, "tls.key")) # verify the that the signed tls.crt is issued by the ca_cert (root cert) - with open(os.path.join("tls-cluster-namespace", "tls.crt"), "r") as f: + with open(os.path.join(tls_dir, "tls.crt"), "r") as f: tls_cert = load_pem_x509_certificate(f.read().encode("utf-8")) - with open(os.path.join("tls-cluster-namespace", "ca.crt"), "r") as f: + with open(os.path.join(tls_dir, "ca.crt"), "r") as f: root_cert = load_pem_x509_certificate(f.read().encode("utf-8")) assert tls_cert.verify_directly_issued_by(root_cert) == None @@ -110,49 +118,163 @@ def test_generate_tls_cert_with_ca_key_fallback(mocker): "kubernetes.client.CoreV1Api.read_namespaced_secret", side_effect=secret_ca_retreival_with_ca_key, ) + # Mock _get_tls_base_dir to return CWD for test isolation + mocker.patch( + "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", + return_value=Path(os.getcwd()), + ) generate_tls_cert("cluster2", "namespace2") - assert os.path.exists("tls-cluster2-namespace2") - assert os.path.exists(os.path.join("tls-cluster2-namespace2", "ca.crt")) - assert os.path.exists(os.path.join("tls-cluster2-namespace2", "tls.crt")) - assert os.path.exists(os.path.join("tls-cluster2-namespace2", "tls.key")) + tls_dir = os.path.join(os.getcwd(), "cluster2-namespace2") + assert os.path.exists(tls_dir) + assert os.path.exists(os.path.join(tls_dir, "ca.crt")) + assert os.path.exists(os.path.join(tls_dir, "tls.crt")) + assert os.path.exists(os.path.join(tls_dir, "tls.key")) # verify the that the signed tls.crt is issued by the ca_cert (root cert) - with open(os.path.join("tls-cluster2-namespace2", "tls.crt"), "r") as f: + with open(os.path.join(tls_dir, "tls.crt"), "r") as f: tls_cert = load_pem_x509_certificate(f.read().encode("utf-8")) - with open(os.path.join("tls-cluster2-namespace2", "ca.crt"), "r") as f: + with open(os.path.join(tls_dir, "ca.crt"), "r") as f: root_cert = load_pem_x509_certificate(f.read().encode("utf-8")) assert tls_cert.verify_directly_issued_by(root_cert) == None # Cleanup for this test - os.remove("tls-cluster2-namespace2/ca.crt") - os.remove("tls-cluster2-namespace2/tls.crt") - os.remove("tls-cluster2-namespace2/tls.key") - os.rmdir("tls-cluster2-namespace2") + os.remove(os.path.join(tls_dir, "ca.crt")) + os.remove(os.path.join(tls_dir, "tls.crt")) + os.remove(os.path.join(tls_dir, "tls.key")) + os.rmdir(tls_dir) -def test_export_env(): +def test_export_env(mocker): """ test the function codeflare_sdk.common.utils.generate_ca_cert.export_ev generates the correct outputs """ - tls_dir = "cluster" + # Mock _get_tls_base_dir to return CWD for test isolation + mocker.patch( + "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", + return_value=Path(os.getcwd()), + ) + + cluster_name = "cluster" ns = "namespace" - export_env(tls_dir, ns) + export_env(cluster_name, ns) + + tls_dir = os.path.join(os.getcwd(), f"{cluster_name}-{ns}") + assert os.environ["RAY_USE_TLS"] == "1" - assert os.environ["RAY_TLS_SERVER_CERT"] == os.path.join( - os.getcwd(), f"tls-{tls_dir}-{ns}", "tls.crt" + assert os.environ["RAY_TLS_SERVER_CERT"] == os.path.join(tls_dir, "tls.crt") + assert os.environ["RAY_TLS_SERVER_KEY"] == os.path.join(tls_dir, "tls.key") + assert os.environ["RAY_TLS_CA_CERT"] == os.path.join(tls_dir, "ca.crt") + + +def secret_ca_retreival_generic(secret_name, namespace): + """Generic mock that works with any cluster/namespace""" + ca_private_key_bytes, ca_cert = generate_ca_cert() + data = {"ca.crt": ca_cert, "tls.key": ca_private_key_bytes} + return client.models.V1Secret(data=data) + + +def test_force_regenerate(mocker): + """ + Test that force_regenerate parameter works correctly + """ + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "codeflare_sdk.common.utils.generate_cert.get_secret_name", + return_value="ca-secret-test-regen", + ) + mocker.patch( + "kubernetes.client.CoreV1Api.read_namespaced_secret", + side_effect=secret_ca_retreival_generic, + ) + mocker.patch( + "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", + return_value=Path(os.getcwd()), ) - assert os.environ["RAY_TLS_SERVER_KEY"] == os.path.join( - os.getcwd(), f"tls-{tls_dir}-{ns}", "tls.key" + + # Generate certificates first time + generate_tls_cert("test-regen", "default") + tls_dir = Path(os.getcwd()) / "test-regen-default" + assert tls_dir.exists() + + # Get modification time of tls.crt + import time + + tls_crt = tls_dir / "tls.crt" + first_mtime = tls_crt.stat().st_mtime + + # Wait a moment + time.sleep(0.1) + + # Try to generate again without force_regenerate (should skip) + generate_tls_cert("test-regen", "default", force_regenerate=False) + second_mtime = tls_crt.stat().st_mtime + assert first_mtime == second_mtime, "Should not regenerate without force_regenerate" + + # Wait a moment + time.sleep(0.1) + + # Generate with force_regenerate (should regenerate) + generate_tls_cert("test-regen", "default", force_regenerate=True) + third_mtime = tls_crt.stat().st_mtime + assert third_mtime > second_mtime, "Should regenerate with force_regenerate=True" + + # Cleanup + import shutil + + shutil.rmtree(tls_dir) + + +def test_refresh_tls_cert(mocker): + """ + Test the refresh_tls_cert function + """ + from codeflare_sdk.common.utils.generate_cert import refresh_tls_cert + + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "codeflare_sdk.common.utils.generate_cert.get_secret_name", + return_value="ca-secret-refresh-test", + ) + mocker.patch( + "kubernetes.client.CoreV1Api.read_namespaced_secret", + side_effect=secret_ca_retreival_generic, ) - assert os.environ["RAY_TLS_CA_CERT"] == os.path.join( - os.getcwd(), f"tls-{tls_dir}-{ns}", "ca.crt" + mocker.patch( + "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", + return_value=Path(os.getcwd()), ) + + # Generate initial certificates + generate_tls_cert("refresh-test", "default") + tls_dir = Path(os.getcwd()) / "refresh-test-default" + assert tls_dir.exists() + + # Refresh should remove and regenerate + result = refresh_tls_cert("refresh-test", "default") + assert result == True + assert tls_dir.exists() # Should exist again + + # Cleanup + import shutil + + shutil.rmtree(tls_dir) # Make sure to always keep this function last def test_cleanup(): - os.remove("tls-cluster-namespace/ca.crt") - os.remove("tls-cluster-namespace/tls.crt") - os.remove("tls-cluster-namespace/tls.key") - os.rmdir("tls-cluster-namespace") + """Clean up test certificate directories.""" + import shutil + + # Clean up any remaining test directories + test_dirs = [ + "cluster-namespace", + "cluster2-namespace2", + "test-regen-default", + "refresh-test-default", + ] + + for dir_name in test_dirs: + tls_dir = os.path.join(os.getcwd(), dir_name) + if os.path.exists(tls_dir): + shutil.rmtree(tls_dir) diff --git a/src/codeflare_sdk/ray/cluster/build_ray_cluster.py b/src/codeflare_sdk/ray/cluster/build_ray_cluster.py index 6a3984b1..e7453332 100644 --- a/src/codeflare_sdk/ray/cluster/build_ray_cluster.py +++ b/src/codeflare_sdk/ray/cluster/build_ray_cluster.py @@ -13,8 +13,8 @@ # limitations under the License. """ - This sub-module exists primarily to be used internally by the Cluster object - (in the cluster sub-module) for RayCluster/AppWrapper generation. +This sub-module exists primarily to be used internally by the Cluster object +(in the cluster sub-module) for RayCluster/AppWrapper generation. """ from typing import List, Union, Tuple, Dict from ...common import _kube_api_error_handling @@ -138,9 +138,11 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): "resources": head_resources, }, "template": V1PodTemplateSpec( - metadata=V1ObjectMeta(cluster.config.annotations) - if cluster.config.annotations - else None, + metadata=( + V1ObjectMeta(cluster.config.annotations) + if cluster.config.annotations + else None + ), spec=get_pod_spec( cluster, [get_head_container_spec(cluster)], @@ -160,9 +162,11 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): "resources": worker_resources, }, "template": V1PodTemplateSpec( - metadata=V1ObjectMeta(cluster.config.annotations) - if cluster.config.annotations - else None, + metadata=( + V1ObjectMeta(cluster.config.annotations) + if cluster.config.annotations + else None + ), spec=get_pod_spec( cluster, [get_worker_container_spec(cluster)], @@ -183,9 +187,9 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): gcs_ft_options = {"redisAddress": cluster.config.redis_address} if cluster.config.external_storage_namespace: - gcs_ft_options[ - "externalStorageNamespace" - ] = cluster.config.external_storage_namespace + gcs_ft_options["externalStorageNamespace"] = ( + cluster.config.external_storage_namespace + ) if cluster.config.redis_password_secret: gcs_ft_options["redisPassword"] = { @@ -437,28 +441,18 @@ def head_worker_extended_resources_from_cluster( resource_type = cluster.config.extended_resource_mapping[k] if resource_type in FORBIDDEN_CUSTOM_RESOURCE_TYPES: continue - head_worker_extended_resources[0][ - resource_type - ] = cluster.config.head_extended_resource_requests[ - k - ] + head_worker_extended_resources[ - 0 - ].get( - resource_type, 0 + head_worker_extended_resources[0][resource_type] = ( + cluster.config.head_extended_resource_requests[k] + + head_worker_extended_resources[0].get(resource_type, 0) ) for k in cluster.config.worker_extended_resource_requests.keys(): resource_type = cluster.config.extended_resource_mapping[k] if resource_type in FORBIDDEN_CUSTOM_RESOURCE_TYPES: continue - head_worker_extended_resources[1][ - resource_type - ] = cluster.config.worker_extended_resource_requests[ - k - ] + head_worker_extended_resources[ - 1 - ].get( - resource_type, 0 + head_worker_extended_resources[1][resource_type] = ( + cluster.config.worker_extended_resource_requests[k] + + head_worker_extended_resources[1].get(resource_type, 0) ) return head_worker_extended_resources diff --git a/src/codeflare_sdk/ray/cluster/cluster.py b/src/codeflare_sdk/ray/cluster/cluster.py index 0d756f62..d9cf38ac 100644 --- a/src/codeflare_sdk/ray/cluster/cluster.py +++ b/src/codeflare_sdk/ray/cluster/cluster.py @@ -311,6 +311,7 @@ def down(self): # Automatically clean up TLS certificates if enabled if self.config.cleanup_tls_certs: from codeflare_sdk.common.utils import generate_cert + if generate_cert.cleanup_tls_cert(resource_name, namespace): print(f"TLS certificates for '{resource_name}' have been removed") @@ -477,13 +478,16 @@ def wait_ready(self, timeout: Optional[int] = None, dashboard_check: bool = True # Automatically generate TLS certificates (required for mTLS) try: from codeflare_sdk.common.utils import generate_cert + generate_cert.generate_tls_cert(self.config.name, self.config.namespace) generate_cert.export_env(self.config.name, self.config.namespace) print(f"TLS certificates generated for '{self.config.name}'") except Exception as e: # Don't fail cluster setup if certificate generation fails print(f"Warning: Could not generate TLS certificates: {e}") - print("You can manually generate certificates using generate_cert.generate_tls_cert()") + print( + "You can manually generate certificates using generate_cert.generate_tls_cert()" + ) while dashboard_check: if timeout and time >= timeout: @@ -522,23 +526,23 @@ def cluster_uri(self) -> str: Returns a string containing the cluster's URI. """ return f"ray://{self.config.name}-head-svc.{self.config.namespace}.svc:10001" - + def refresh_certificates(self): """ Refreshes TLS certificates by removing old ones and generating new ones. - + This is useful when: - The server CA secret has been rotated - Certificates have expired - You encounter TLS handshake failures - You need to regenerate certificates for any reason - + The method will: 1. Remove existing client certificates 2. Fetch the latest CA from Kubernetes 3. Generate new client certificates 4. Update environment variables for Ray - + Example: >>> # If you get TLS errors after CA rotation >>> cluster.refresh_certificates() @@ -546,13 +550,13 @@ def refresh_certificates(self): >>> ray.init(address=cluster.cluster_uri()) """ from codeflare_sdk.common.utils import generate_cert - + print(f"Refreshing TLS certificates for '{self.config.name}'...") - + # Use the refresh function which handles cleanup and regeneration generate_cert.refresh_tls_cert(self.config.name, self.config.namespace) generate_cert.export_env(self.config.name, self.config.namespace) - + print(f"✓ TLS certificates refreshed for '{self.config.name}'") def cluster_dashboard_uri(self) -> str: diff --git a/src/codeflare_sdk/ray/cluster/test_cluster.py b/src/codeflare_sdk/ray/cluster/test_cluster.py index ed07cbaf..f5d2bb7b 100644 --- a/src/codeflare_sdk/ray/cluster/test_cluster.py +++ b/src/codeflare_sdk/ray/cluster/test_cluster.py @@ -73,10 +73,18 @@ def test_cluster_apply_down(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) + # Mock certificate cleanup (automatic in cluster.down()) + mock_cleanup = mocker.patch( + "codeflare_sdk.common.utils.generate_cert.cleanup_tls_cert", return_value=True + ) + cluster = create_cluster(mocker) cluster.apply() cluster.down() + # Verify cleanup was called + mock_cleanup.assert_called_once_with("unit-test-cluster", "ns") + def test_cluster_apply_scale_up_scale_down(mocker): mocker.patch("kubernetes.client.ApisApi.get_api_versions") @@ -118,6 +126,11 @@ def test_cluster_apply_scale_up_scale_down(mocker): patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) cluster.apply() + # Mock certificate cleanup (automatic in cluster.down()) + mocker.patch( + "codeflare_sdk.common.utils.generate_cert.cleanup_tls_cert", return_value=True + ) + # Tear down cluster.down() @@ -147,6 +160,11 @@ def test_cluster_apply_with_file(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_obj_none("ray.io", "v1", "ns", "rayclusters"), ) + # Mock certificate cleanup (automatic in cluster.down()) + mocker.patch( + "codeflare_sdk.common.utils.generate_cert.cleanup_tls_cert", return_value=True + ) + cluster.apply() # Tear down cluster.down() @@ -283,6 +301,11 @@ def test_cluster_apply_down_no_mcad(mocker): "kubernetes.client.CustomObjectsApi.list_cluster_custom_object", return_value={"items": []}, ) + # Mock certificate cleanup (automatic in cluster.down()) + mocker.patch( + "codeflare_sdk.common.utils.generate_cert.cleanup_tls_cert", return_value=True + ) + config = create_cluster_config() config.name = "unit-test-cluster-ray" config.appwrapper = False @@ -525,6 +548,11 @@ def test_wait_ready(mocker, capsys): mock_response = mocker.Mock() mock_response.status_code = 200 mocker.patch("requests.get", return_value=mock_response) + + # Mock certificate generation (automatic in wait_ready) + mocker.patch("codeflare_sdk.common.utils.generate_cert.generate_tls_cert") + mocker.patch("codeflare_sdk.common.utils.generate_cert.export_env") + cf = Cluster( ClusterConfiguration( name="test", @@ -552,13 +580,13 @@ def test_wait_ready(mocker, capsys): captured = capsys.readouterr() assert ( captured.out - == "Waiting for requested resources to be set up...\nRequested cluster is up and running!\nDashboard is ready!\n" + == "Waiting for requested resources to be set up...\nRequested cluster is up and running!\nTLS certificates generated for 'test'\nDashboard is ready!\n" ) cf.wait_ready(dashboard_check=False) captured = capsys.readouterr() assert ( captured.out - == "Waiting for requested resources to be set up...\nRequested cluster is up and running!\n" + == "Waiting for requested resources to be set up...\nRequested cluster is up and running!\nTLS certificates generated for 'test'\n" ) diff --git a/src/codeflare_sdk/ray/rayjobs/rayjob.py b/src/codeflare_sdk/ray/rayjobs/rayjob.py index c06c596e..2b18e0e4 100644 --- a/src/codeflare_sdk/ray/rayjobs/rayjob.py +++ b/src/codeflare_sdk/ray/rayjobs/rayjob.py @@ -281,9 +281,9 @@ def _build_rayjob_cr(self) -> Dict[str, Any]: # Add submitterPodTemplate if we have files to mount if files: secret_name = f"{self.name}-files" - rayjob_cr["spec"][ - "submitterPodTemplate" - ] = self._build_submitter_pod_template(files, secret_name) + rayjob_cr["spec"]["submitterPodTemplate"] = ( + self._build_submitter_pod_template(files, secret_name) + ) # Configure cluster: either use existing or create new if self._cluster_config is not None: From 38a22d35c5823c4fc27f99b924232ca3d092ff3f Mon Sep 17 00:00:00 2001 From: Laura Fitzgerald Date: Wed, 26 Nov 2025 12:00:26 +0000 Subject: [PATCH 4/4] precommit fixes --- .../common/utils/test_generate_cert.py | 6 ++-- .../ray/cluster/build_ray_cluster.py | 28 +++++++++++++------ src/codeflare_sdk/ray/rayjobs/rayjob.py | 6 ++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/codeflare_sdk/common/utils/test_generate_cert.py b/src/codeflare_sdk/common/utils/test_generate_cert.py index c1ef83c0..082550be 100644 --- a/src/codeflare_sdk/common/utils/test_generate_cert.py +++ b/src/codeflare_sdk/common/utils/test_generate_cert.py @@ -191,7 +191,7 @@ def test_force_regenerate(mocker): "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", return_value=Path(os.getcwd()), ) - + # Generate certificates first time generate_tls_cert("test-regen", "default") tls_dir = Path(os.getcwd()) / "test-regen-default" @@ -230,7 +230,7 @@ def test_refresh_tls_cert(mocker): Test the refresh_tls_cert function """ from codeflare_sdk.common.utils.generate_cert import refresh_tls_cert - + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") mocker.patch( "codeflare_sdk.common.utils.generate_cert.get_secret_name", @@ -244,7 +244,7 @@ def test_refresh_tls_cert(mocker): "codeflare_sdk.common.utils.generate_cert._get_tls_base_dir", return_value=Path(os.getcwd()), ) - + # Generate initial certificates generate_tls_cert("refresh-test", "default") tls_dir = Path(os.getcwd()) / "refresh-test-default" diff --git a/src/codeflare_sdk/ray/cluster/build_ray_cluster.py b/src/codeflare_sdk/ray/cluster/build_ray_cluster.py index e7453332..cf52b697 100644 --- a/src/codeflare_sdk/ray/cluster/build_ray_cluster.py +++ b/src/codeflare_sdk/ray/cluster/build_ray_cluster.py @@ -187,9 +187,9 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): gcs_ft_options = {"redisAddress": cluster.config.redis_address} if cluster.config.external_storage_namespace: - gcs_ft_options["externalStorageNamespace"] = ( - cluster.config.external_storage_namespace - ) + gcs_ft_options[ + "externalStorageNamespace" + ] = cluster.config.external_storage_namespace if cluster.config.redis_password_secret: gcs_ft_options["redisPassword"] = { @@ -441,18 +441,28 @@ def head_worker_extended_resources_from_cluster( resource_type = cluster.config.extended_resource_mapping[k] if resource_type in FORBIDDEN_CUSTOM_RESOURCE_TYPES: continue - head_worker_extended_resources[0][resource_type] = ( - cluster.config.head_extended_resource_requests[k] - + head_worker_extended_resources[0].get(resource_type, 0) + head_worker_extended_resources[0][ + resource_type + ] = cluster.config.head_extended_resource_requests[ + k + ] + head_worker_extended_resources[ + 0 + ].get( + resource_type, 0 ) for k in cluster.config.worker_extended_resource_requests.keys(): resource_type = cluster.config.extended_resource_mapping[k] if resource_type in FORBIDDEN_CUSTOM_RESOURCE_TYPES: continue - head_worker_extended_resources[1][resource_type] = ( - cluster.config.worker_extended_resource_requests[k] - + head_worker_extended_resources[1].get(resource_type, 0) + head_worker_extended_resources[1][ + resource_type + ] = cluster.config.worker_extended_resource_requests[ + k + ] + head_worker_extended_resources[ + 1 + ].get( + resource_type, 0 ) return head_worker_extended_resources diff --git a/src/codeflare_sdk/ray/rayjobs/rayjob.py b/src/codeflare_sdk/ray/rayjobs/rayjob.py index 2b18e0e4..c06c596e 100644 --- a/src/codeflare_sdk/ray/rayjobs/rayjob.py +++ b/src/codeflare_sdk/ray/rayjobs/rayjob.py @@ -281,9 +281,9 @@ def _build_rayjob_cr(self) -> Dict[str, Any]: # Add submitterPodTemplate if we have files to mount if files: secret_name = f"{self.name}-files" - rayjob_cr["spec"]["submitterPodTemplate"] = ( - self._build_submitter_pod_template(files, secret_name) - ) + rayjob_cr["spec"][ + "submitterPodTemplate" + ] = self._build_submitter_pod_template(files, secret_name) # Configure cluster: either use existing or create new if self._cluster_config is not None: