diff --git a/larky/src/main/resources/vendor/jose/backends/pycrypto_backend.star b/larky/src/main/resources/vendor/jose/backends/pycrypto_backend.star index f14fc61d8..a7a5e696e 100644 --- a/larky/src/main/resources/vendor/jose/backends/pycrypto_backend.star +++ b/larky/src/main/resources/vendor/jose/backends/pycrypto_backend.star @@ -157,38 +157,6 @@ def RSAKey(key, algorithm): SHA512=SHA512, SHA1=SHA1) - def __init__(key, algorithm): - if not operator.contains(ALGORITHMS.RSA, algorithm): - return Error("JWKError: hash_alg: %s is not a valid hash algorithm" % algorithm).unwrap() - - self.hash_alg = { - ALGORITHMS.RS256: self.SHA256, - ALGORITHMS.RS384: self.SHA384, - ALGORITHMS.RS512: self.SHA512, - ALGORITHMS.RSA1_5: self.SHA1, - ALGORITHMS.RSA_OAEP: self.SHA1, - ALGORITHMS.RSA_OAEP_256: self.SHA256, - }.get(algorithm) - self._algorithm = algorithm - - if types.is_instance(key, _RSAKey): - self.prepared_key = key - return self - - if types.is_dict(key): - self._process_jwk(key) - return self - - if types.is_string(key): - key = codecs.encode(key, encoding='utf-8') - - if types.is_bytelike(key): - self.prepared_key = RSA.importKey(key) - return self - - return Error("JWKError: Unable to parse an RSA_JWK from key: %s" % key).unwrap() - self = __init__(key, algorithm) - def _process_jwk(jwk_dict): if not jwk_dict.get('kty') == 'RSA': return Error("JWKError: Incorrect key type. Expected: 'RSA', Received: %s" % jwk_dict.get('kty')) @@ -227,6 +195,38 @@ def RSAKey(key, algorithm): return self.prepared_key self._process_jwk = _process_jwk + def __init__(key, algorithm): + if not operator.contains(ALGORITHMS.RSA, algorithm): + return Error("JWKError: hash_alg: %s is not a valid hash algorithm" % algorithm).unwrap() + + self.hash_alg = { + ALGORITHMS.RS256: self.SHA256, + ALGORITHMS.RS384: self.SHA384, + ALGORITHMS.RS512: self.SHA512, + ALGORITHMS.RSA1_5: self.SHA1, + ALGORITHMS.RSA_OAEP: self.SHA1, + ALGORITHMS.RSA_OAEP_256: self.SHA256, + }.get(algorithm) + self._algorithm = algorithm + + if types.is_instance(key, _RSAKey): + self.prepared_key = key + return self + + if types.is_dict(key): + self._process_jwk(key) + return self + + if types.is_string(key): + key = codecs.encode(key, encoding='utf-8') + + if types.is_bytelike(key): + self.prepared_key = RSA.importKey(key) + return self + + return Error("JWKError: Unable to parse an RSA_JWK from key: %s" % key).unwrap() + self = __init__(key, algorithm) + def _process_cert(key): pemLines = key.replace(b' ', b'').split() certDer = base64url_decode(b''.join(pemLines[1:-1])) @@ -377,6 +377,7 @@ def AESKey(key, algorithm): ALGORITHMS.A256GCM: AES.MODE_GCM, } + self.IV_BYTE_LENGTH_MODE_MAP = {AES.MODE_CBC: AES.block_size // 8, AES.MODE_GCM: 96 // 8} def __init__(key, algorithm): if not operator.contains(ALGORITHMS.AES, algorithm): @@ -416,7 +417,8 @@ def AESKey(key, algorithm): def encrypt(plain_text, aad=None): plain_text = six.ensure_binary(plain_text) def _try_encrypt(self, plain_text, aad): - iv = get_random_bytes(AES.block_size) + iv_byte_length = self.IV_BYTE_LENGTH_MODE_MAP.get(self._mode, AES.block_size) + iv = get_random_bytes(iv_byte_length) cipher = AES.new(self._key, self._mode, iv) if self._mode == AES.MODE_CBC: padded_plain_text = self._pad(AES.block_size, plain_text) diff --git a/larky/src/main/resources/vendor/jose/utils.star b/larky/src/main/resources/vendor/jose/utils.star index 92dd4883e..cb7d0f536 100644 --- a/larky/src/main/resources/vendor/jose/utils.star +++ b/larky/src/main/resources/vendor/jose/utils.star @@ -1,8 +1,10 @@ load("@stdlib//base64", base64="base64") +load("@stdlib//binascii", hexlify="hexlify") load("@stdlib//builtins", builtins="builtins") load("@stdlib//codecs", codecs="codecs") load("@stdlib//struct", struct="struct") load("@stdlib//types", types="types") +load("@vendor//Crypto/Util/py3compat", tostr="tostr") # Piggyback of the backends implementation of the function that converts a long # to a bytes stream. Some plumbing is necessary to have the signatures match. @@ -25,7 +27,7 @@ def long_to_base64(data, size=0): def int_arr_to_long(arr): - return int(''.join(["%02x" % byte for byte in arr]), 16) + return int(tostr(hexlify(arr)), 16) def base64_to_long(data): @@ -34,7 +36,7 @@ def base64_to_long(data): # urlsafe_b64decode will happily convert b64encoded data _d = base64.urlsafe_b64decode(bytes(data) + b"==") - return int_arr_to_long(struct.unpack("%sB" % len(_d), _d)) + return int_arr_to_long(_d) def calculate_at_hash(access_token, hash_alg): diff --git a/larky/src/test/resources/vendor_tests/jose/test_jose.star b/larky/src/test/resources/vendor_tests/jose/test_jose.star index 42a4dea52..841d39be8 100644 --- a/larky/src/test/resources/vendor_tests/jose/test_jose.star +++ b/larky/src/test/resources/vendor_tests/jose/test_jose.star @@ -23,6 +23,45 @@ def test_encrypt_and_decrypt_jwe_with_defaults(): asserts.eq(decrypted_data, b'533') +def test_decrypt_jwk(): + jwk_data = json.loads(""" + { + "kty": "RSA", + "kid": "qfNlZYnIBmF_1JyVr14wlu34pDNQ9ggEjC6HiDQkDks", + "n": "uo80-yn_zR3vrRWFunyLTm7NFKO0aWFH1LsdkRPm62w13AA1btO89IxncW56L8qIAX7xBlCt92TB4mVnGEzWUbPLyn-OPyg5sgltsoAjFcYeFc0uvp0-iRMse4udjwk0YqrCgAOwlcL5IQu9KmqH3KdH8o1Um9QtoBxHe4ACWMdu8I2Xktm2vi076opkT2AquqLXRzD4gujAALgTMnGmBmNSqhKa2GK4iSjqUSTkYL6wF_OtPfdmlrlri_qaChRk-kK1VzIDk8jG_-I8d47_Pdln7FmADhO1O4_-4IoOiM1ESarNkO46aFV4I4xcFWvh0JnJQIoSwgPoqCPH3v7NiQ", + "e": "AQAB", + "alg": "RSA-OAEP-256", + "use": "enc" + } + """) + + jwe_data = """ + { + "user": { + "name": { + "first_name": "Test", + "last_name": "Test" + }, + "address": { + "street": "Street1", + "street2": "Apt.#1", + "city": "NewYork", + "region": "NY", + "postal_code": "12345", + "country": "US" + }, + "phone_number": "+15174242424" + }, + "card": { + "number": "4242424242424242", + "expiration": "07/26", + "cvv": "012" + } + } + """ + + jwe.encrypt(jwe_data, jwk_data, 'A256GCM', jwk_data.get("alg"), None, None, jwk_data.get("kid")) + def test_decrypt_GCM256_AES_wrapped_key_jwe(): jweString = b"eyJlbmMiOiJBMjU2R0NNIiwidGFnIjoiazhaNnpTNjRJclllaUNpNV9JaWY5QSIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoieW9sTk8xLVFXSVg3R1poSCJ9.knPF3qV22v0pE-N6oUzlSIoBUEjr_k3sfFyYX-XuSH8.XkADjU0P2phiynPA.cvCd.Wp5oFTRAzEIFN8pImDnhmw" encryptionKey = b'96a18c1acc0b48beb9b24479355b70b5' @@ -308,6 +347,7 @@ def test_encrypt_with_extra_headers(): def _testsuite(): _suite = unittest.TestSuite() + _suite.addTest(unittest.FunctionTestCase(test_decrypt_jwk)) _suite.addTest(unittest.FunctionTestCase(test_encrypt_and_decrypt_jwe_with_defaults)) _suite.addTest(unittest.FunctionTestCase(test_decrypt_GCM256_AES_wrapped_key_jwe)) larky.parametrize( diff --git a/poetry.lock b/poetry.lock index 489a89078..6779ba2ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "colorama" @@ -6,6 +6,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -13,13 +15,15 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -31,6 +35,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -38,24 +43,26 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -68,6 +75,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -84,18 +92,71 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "setuptools" +version = "74.1.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "setuptools-74.1.3-py3-none-any.whl", hash = "sha256:1cfd66bfcf197bce344da024c8f5b35acc4dcb7ca5202246a75296b4883f6851"}, + {file = "setuptools-74.1.3.tar.gz", hash = "sha256:fbb126f14b0b9ffa54c4574a50ae60673bbe8ae0b1645889d10b3b14f5891d28"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] + [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "ef0aabdf94ba914eac7615ab7f36579942be0fc0f8bcdf14467ebdb2a20e9bbd" +content-hash = "01bd15d9eeccfe8637f28c3f5c49af7943c375946002f6e716a6e4761f9fc0c9" diff --git a/pyproject.toml b/pyproject.toml index 00dde9492..f1c87d56e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,9 @@ readme = "pylarky/README.md" [tool.poetry.dependencies] python = "^3.10" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pytest = "^7.4.4" +setuptools = "^74.1.2" [build-system] requires = ["setuptools", "poetry-core>=1.0.0"]