initial commit
This commit is contained in:
commit
7af6e935ee
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# Rust
|
||||
target
|
||||
|
||||
# Python / Maturin
|
||||
python/__pycache__
|
||||
python/*.cpython*
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
tests/__pycache__
|
||||
793
Cargo.lock
generated
Normal file
793
Cargo.lock
generated
Normal file
@ -0,0 +1,793 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381_plus"
|
||||
version = "0.8.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa37cf2a8c96054d2dc3d708efe35cc0347014af0d30b86c736b4388ff8491c"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"elliptic-curve",
|
||||
"ff",
|
||||
"group",
|
||||
"hex",
|
||||
"pairing",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20poly1305"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rand_core",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
|
||||
dependencies = [
|
||||
"group",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a"
|
||||
dependencies = [
|
||||
"indoc",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"once_cell",
|
||||
"portable-atomic",
|
||||
"pyo3-build-config",
|
||||
"pyo3-ffi",
|
||||
"pyo3-macros",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x25519-dalek"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zkac"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"chacha20poly1305",
|
||||
"curve25519-dalek",
|
||||
"digest",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"pyo3",
|
||||
"rand",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
"zkryptium",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zkryptium"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39159a1cd33b28bad7c3502528f77da679dd6f45a055581f5ac56954c458c6e5"
|
||||
dependencies = [
|
||||
"bls12_381_plus",
|
||||
"digest",
|
||||
"elliptic-curve",
|
||||
"ff",
|
||||
"group",
|
||||
"hex",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sha3",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
28
Cargo.toml
Normal file
28
Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "zkac"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Zero-Knowledge Access Control: BBS+ anonymous credentials (BLS12-381) with encrypted transport (X25519/ChaCha20-Poly1305)"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
curve25519-dalek = { version = "4", features = ["rand_core", "digest", "zeroize"] }
|
||||
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||
chacha20poly1305 = "0.10"
|
||||
blake2 = "0.10"
|
||||
digest = "0.10"
|
||||
hkdf = "0.12"
|
||||
sha2 = "0.10"
|
||||
rand = "0.8"
|
||||
subtle = "2"
|
||||
zeroize = { version = "1", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
zkryptium = { version = "0.6", default-features = false, features = ["bbsplus", "bbsplus_blind"] }
|
||||
hex = "0.4"
|
||||
pyo3 = { version = "0.25", features = ["extension-module"], optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
python = ["pyo3"]
|
||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# ZKAC
|
||||
|
||||
**Zero-Knowledge Access Control** — BBS+ anonymous credentials on BLS12-381 with an encrypted session layer (X25519, ChaCha20-Poly1305, replay protection).
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Python API](docs/PYTHON_API.md)** — types and usage for `import zkac`
|
||||
- **[Security model](SECURITY.md)** — threat model, assumptions, operational guidance
|
||||
|
||||
## Rust
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
zkac = { path = "..." }
|
||||
```
|
||||
|
||||
Public API highlights: `zkac::Node`, `zkac::Credential`, `zkac::RoleRegistry`, `zkac::IssuerKeyPair`, `zkac::MAX_BBS_AUTH_PROOF_BYTES`.
|
||||
|
||||
## Python
|
||||
|
||||
Requires Rust toolchain and [maturin](https://www.maturin.rs/).
|
||||
|
||||
```bash
|
||||
uv venv && source .venv/bin/activate
|
||||
maturin develop --features python
|
||||
python -c "import zkac; print(zkac.role_id('admin').hex())"
|
||||
```
|
||||
|
||||
Run tests: `cargo test` and `pytest tests/test_zkac.py`.
|
||||
|
||||
## License
|
||||
|
||||
See repository license file (if present).
|
||||
96
docs/PYTHON_API.md
Normal file
96
docs/PYTHON_API.md
Normal file
@ -0,0 +1,96 @@
|
||||
# ZKAC Python API Reference
|
||||
|
||||
Version 0.1. Cryptographic stack: **BBS+** on BLS12-381 (credentials), **X25519** + **ChaCha20-Poly1305** (transport), **BLAKE2b** (role IDs).
|
||||
|
||||
```python
|
||||
import zkac
|
||||
```
|
||||
|
||||
## Constants
|
||||
|
||||
### `MAX_BBS_AUTH_PROOF_BYTES`
|
||||
|
||||
Upper bound on BBS+ proof size in an encrypted auth packet (256 KiB). Larger proofs are rejected.
|
||||
|
||||
## Transport identity (Ristretto255)
|
||||
|
||||
### `Keypair()`
|
||||
|
||||
Generates a new random keypair.
|
||||
|
||||
### `Keypair.public_key() -> PublicKey`
|
||||
|
||||
Raises `ValueError` if consumed by `Node(...)`.
|
||||
|
||||
### `PublicKey`
|
||||
|
||||
- `to_bytes() -> bytes` — 32 bytes
|
||||
- `from_bytes(bytes) -> PublicKey`
|
||||
- Equality, hash, `repr` supported
|
||||
|
||||
## BBS+ credentials
|
||||
|
||||
### `role_id(name: str) -> bytes`
|
||||
|
||||
32-byte opaque role id from a human-readable name.
|
||||
|
||||
### `BbsIssuer()`
|
||||
|
||||
New random issuer. `from_secret_key(bytes)` restores from 32-byte secret.
|
||||
|
||||
- `public_key() -> BbsPublicKey`
|
||||
- `secret_key_bytes() -> bytes` (confidential)
|
||||
- `issue_blind(commitment_with_proof, role_id, epoch) -> bytes` — `role_id` must be 32 bytes
|
||||
|
||||
### `BbsPublicKey`
|
||||
|
||||
- `to_bytes() / from_bytes`
|
||||
|
||||
### `prepare_blind_request() -> BlindRequest`
|
||||
|
||||
- `commitment_with_proof()`, `member_secret()`, `prover_blind()` — all return `bytes`
|
||||
|
||||
### `Credential.finalize(blind_sig, member_secret, prover_blind, role_id, epoch, pk) -> Credential`
|
||||
|
||||
- `present(nonce) -> bytes`
|
||||
- `role_id() -> bytes`, `epoch() -> int`
|
||||
|
||||
## Server registry
|
||||
|
||||
### `RoleRegistry`
|
||||
|
||||
- `register_role(role_id, pk, epoch)` — `role_id` 32 bytes
|
||||
- `set_epoch(role_id, epoch)`
|
||||
- `verify_presentation(role_id, proof_bytes, nonce) -> bool`
|
||||
- `has_role(role_id) -> bool`
|
||||
|
||||
## Node and session
|
||||
|
||||
### `Node(keypair)` — consumes `Keypair`
|
||||
|
||||
- `public_key() -> PublicKey`
|
||||
- `connect() -> (PendingConnect, bytes)` — 32-byte init message
|
||||
- `accept(init_msg) -> (Session, bytes)` — `init_msg` 32 bytes
|
||||
- `complete_connect(pending, response_msg, credential) -> (Session, bytes)` — `response_msg` 32 bytes
|
||||
- `verify_auth(session, encrypted_auth, registry) -> bytes` — returns 32-byte `role_id`
|
||||
|
||||
### `Session`
|
||||
|
||||
- `transcript_hash() -> bytes` — use as `nonce` for `Credential.present`
|
||||
- `encrypt(plaintext) -> bytes`
|
||||
- `decrypt(packet) -> bytes`
|
||||
|
||||
## Typical flow
|
||||
|
||||
1. Issuer creates `BbsIssuer()`; server `register_role(role_id, issuer.public_key(), epoch)`.
|
||||
2. Member: `prepare_blind_request` → issuer `issue_blind` → `Credential.finalize`.
|
||||
3. Client: `connect` → server `accept` → client `complete_connect` → server `verify_auth`.
|
||||
4. Use `Session.encrypt` / `decrypt` for data.
|
||||
|
||||
## Errors
|
||||
|
||||
Raises `ValueError` with descriptive messages for crypto failures, replay, and bad inputs.
|
||||
|
||||
## Further reading
|
||||
|
||||
[Security model and assumptions](../SECURITY.md)
|
||||
75
docs/SECURITY.md
Normal file
75
docs/SECURITY.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Security model and audit notes (ZKAC 0.1)
|
||||
|
||||
This document summarizes the design, residual risks, and recommendations for operators integrating **ZKAC**. It is not a substitute for independent review before high-assurance deployment.
|
||||
|
||||
## Goals
|
||||
|
||||
- **Authentication:** Only holders of a valid BBS+ credential for a registered role can complete `verify_auth` for that role.
|
||||
- **Confidentiality & integrity:** Session payloads are authenticated-encrypted (ChaCha20-Poly1305) with a key derived from an ephemeral X25519 handshake.
|
||||
- **Replay resistance:** Duplicate ciphertexts in a direction are rejected (sliding window + monotonic counter).
|
||||
- **Unlinkability (credential layer):** BBS+ presentations are intended to be unlinkable across sessions when the presentation header (here, the session transcript hash) differs; the verifier learns only the disclosed attributes (opaque `role_id`, epoch) and validity.
|
||||
- **Server cannot forge credentials:** The server stores only the issuer **public** key per role; forging requires the issuer secret key.
|
||||
|
||||
## Cryptographic components
|
||||
|
||||
| Layer | Primitive | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| Transport | X25519 ephemeral DH, HKDF-SHA256, ChaCha20-Poly1305 | Session keys, AEAD |
|
||||
| Credentials | BBS+ on BLS12-381 (zkryptium), SHAKE256 ciphersuite | Blind issuance, ZK presentations |
|
||||
| Role IDs | BLAKE2b-512 (truncated to 32 bytes) | Opaque role identifiers |
|
||||
|
||||
## Threats considered
|
||||
|
||||
### Network attacker (passive)
|
||||
|
||||
- Observes ciphertexts; cannot break ChaCha20-Poly1305 or derive session keys without breaking X25519 / HKDF under standard assumptions.
|
||||
|
||||
### Network attacker (active / MITM)
|
||||
|
||||
- **Without binding the handshake to authenticated long-term keys:** A MITM can run its own X25519 exchange with each side and decrypt/modify traffic unless an **out-of-band** binding exists (e.g. TLS with server authentication, or clients verifying server `PublicKey` through a trusted channel). The library’s `Keypair` / `PublicKey` are available for application-level identity display or future binding; **this release does not sign the handshake with long-term keys**.
|
||||
- **Recommendation:** Run the protocol inside **TLS 1.3** (or similar) for production, or add an explicit long-term key signing step over the transcript if you need standalone MITM resistance.
|
||||
|
||||
### Malicious server
|
||||
|
||||
- Can **learn** opaque `role_id`, current epoch, and that *some* valid member authenticated.
|
||||
- **Cannot** forge BBS+ credentials without the issuer secret key.
|
||||
- **Cannot** learn `member_secret` from presentations under the BBS+ security assumptions.
|
||||
- **Cannot** distinguish which specific member authenticated among valid credential holders (unlinkability holds against the verifier for distinct presentation headers).
|
||||
|
||||
### Malicious client
|
||||
|
||||
- Cannot decrypt others’ traffic without session keys.
|
||||
- Cannot produce valid auth for a role without a valid credential + correct epoch + registry entry.
|
||||
|
||||
### Denial of service
|
||||
|
||||
- **Auth packet size:** Proof length is capped (`MAX_BBS_AUTH_PROOF_BYTES`, 256 KiB) to bound allocations.
|
||||
- **Handshake:** Fixed 32-byte messages; no variable-length handshake parsing.
|
||||
- General packet limits should still be enforced at the application layer (total message size, rate limits).
|
||||
|
||||
## Operational requirements
|
||||
|
||||
1. **Issuer secret key:** Protect `BbsIssuer` secret material (HSM, KMS, or encrypted at rest). Compromise = ability to issue arbitrary credentials for that role.
|
||||
2. **Member storage:** `member_secret` and finalized `Credential` material must be protected; loss = re-enrollment required.
|
||||
3. **Epoch revocation:** On compromise or policy change, call `set_epoch` and re-issue credentials only to legitimate members; old credentials become invalid at verification time.
|
||||
4. **Registry integrity:** The server’s `(role_id → public key, epoch)` mapping must be integrity-protected (trusted storage or signed updates), or attackers could swap keys or epochs.
|
||||
5. **Role ID privacy:** `role_id` is a hash of the role name only if you use `role_id("myrole")`; treat role names as secrets if enumeration is a concern, or derive role IDs with an additional secret salt known to members.
|
||||
|
||||
## Implementation notes (audit checklist)
|
||||
|
||||
- [x] BBS+ proof verification uses the same header and presentation binding as proof generation (`verify_presentation` in Rust).
|
||||
- [x] Session transcript is included in the presentation via `present(transcript_hash)`.
|
||||
- [x] Replay protection is symmetric per direction in `Session`.
|
||||
- [x] Constant-time comparisons are used where critical in transport/replay paths (`subtle` crate).
|
||||
- [ ] **External:** Python bindings surface raw bytes; callers must not log secrets (`secret_key_bytes`, `member_secret`, `prover_blind`).
|
||||
- [ ] **External:** Use secure randomness from the OS (library uses OS RNG for key generation paths exposed in Rust).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **No post-quantum** primitives: classical security assumptions only.
|
||||
- **Epoch granularity:** Revocation is coarse (epoch bump); plan issuance and rotation policy accordingly.
|
||||
- **zkryptium dependency:** Security follows the underlying crate and BLS12-381/BBS+ standards; keep dependencies updated.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Report security-sensitive findings through your project’s private disclosure channel (configure `SECURITY.md` contact or GitHub security advisories when the repository is public).
|
||||
37
python/zkac/__init__.py
Normal file
37
python/zkac/__init__.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
ZKAC — Zero-Knowledge Access Control
|
||||
|
||||
BBS+ anonymous credentials (BLS12-381) with encrypted transport (Ristretto255 / X25519).
|
||||
"""
|
||||
|
||||
from zkac._zkac import (
|
||||
MAX_BBS_AUTH_PROOF_BYTES,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
BbsIssuer,
|
||||
BbsPublicKey,
|
||||
BlindRequest,
|
||||
Credential,
|
||||
prepare_blind_request,
|
||||
role_id,
|
||||
RoleRegistry,
|
||||
Session,
|
||||
Node,
|
||||
PendingConnect,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MAX_BBS_AUTH_PROOF_BYTES",
|
||||
"Keypair",
|
||||
"PublicKey",
|
||||
"BbsIssuer",
|
||||
"BbsPublicKey",
|
||||
"BlindRequest",
|
||||
"Credential",
|
||||
"prepare_blind_request",
|
||||
"role_id",
|
||||
"RoleRegistry",
|
||||
"Session",
|
||||
"Node",
|
||||
"PendingConnect",
|
||||
]
|
||||
406
src/credential/bbs.rs
Normal file
406
src/credential/bbs.rs
Normal file
@ -0,0 +1,406 @@
|
||||
use blake2::Blake2b512;
|
||||
use digest::Digest;
|
||||
use zkryptium::{
|
||||
bbsplus::commitment::BlindFactor,
|
||||
bbsplus::keys::{BBSplusPublicKey, BBSplusSecretKey},
|
||||
keys::pair::KeyPair,
|
||||
schemes::algorithms::BbsBls12381Shake256,
|
||||
schemes::generics::{BlindSignature, Commitment, PoKSignature},
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
const ROLE_ID_DOMAIN: &[u8] = b"zkac-role-id-v1";
|
||||
const BBS_HEADER: &[u8] = b"zkac-bbs-credential-v1";
|
||||
|
||||
type CS = BbsBls12381Shake256;
|
||||
|
||||
/// BBS+ issuer keypair held by the role creator.
|
||||
/// The secret key signs credentials; the public key is given to the server.
|
||||
pub struct IssuerKeyPair {
|
||||
sk: BBSplusSecretKey,
|
||||
pk: BBSplusPublicKey,
|
||||
}
|
||||
|
||||
/// Serializable BBS+ public key stored by the server for verification.
|
||||
#[derive(Clone)]
|
||||
pub struct IssuerPublicKey {
|
||||
inner: BBSplusPublicKey,
|
||||
}
|
||||
|
||||
/// Member-side blinding output: commitment bytes + prover blind factor.
|
||||
pub struct BlindRequest {
|
||||
pub commitment_with_proof: Vec<u8>,
|
||||
pub prover_blind: BlindFactor,
|
||||
pub member_secret: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A finalized credential held by a member.
|
||||
pub struct Credential {
|
||||
blind_sig_bytes: [u8; 80],
|
||||
member_secret: Vec<u8>,
|
||||
prover_blind: BlindFactor,
|
||||
role_id: [u8; 32],
|
||||
epoch: u64,
|
||||
pk: BBSplusPublicKey,
|
||||
}
|
||||
|
||||
/// A ZK presentation proving credential possession.
|
||||
pub struct Presentation {
|
||||
proof_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Derive a deterministic 32-byte role identifier from a human-readable name.
|
||||
pub fn role_id(name: &str) -> [u8; 32] {
|
||||
let mut h = Blake2b512::new();
|
||||
h.update(ROLE_ID_DOMAIN);
|
||||
h.update(name.as_bytes());
|
||||
let full: [u8; 64] = h.finalize().into();
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(&full[..32]);
|
||||
out
|
||||
}
|
||||
|
||||
fn epoch_to_msg(epoch: u64) -> Vec<u8> {
|
||||
epoch.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn issuer_messages(role_id: &[u8; 32], epoch: u64) -> Vec<Vec<u8>> {
|
||||
vec![role_id.to_vec(), epoch_to_msg(epoch)]
|
||||
}
|
||||
|
||||
impl IssuerKeyPair {
|
||||
pub fn generate() -> Result<Self> {
|
||||
let kp = KeyPair::<CS>::random()
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
let (sk, pk) = kp.into_parts();
|
||||
Ok(Self { sk, pk })
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> IssuerPublicKey {
|
||||
IssuerPublicKey { inner: self.pk.clone() }
|
||||
}
|
||||
|
||||
pub fn secret_key_bytes(&self) -> [u8; 32] {
|
||||
self.sk.to_bytes()
|
||||
}
|
||||
|
||||
pub fn from_secret_key_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
let sk = BBSplusSecretKey::from_bytes(bytes)
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
let pk = sk.public_key();
|
||||
Ok(Self { sk, pk })
|
||||
}
|
||||
|
||||
/// Blind-sign a commitment produced by a member.
|
||||
/// The issuer knows role_id and epoch but NOT the member_secret.
|
||||
pub fn issue_blind(
|
||||
&self,
|
||||
commitment_with_proof: &[u8],
|
||||
role_id: &[u8; 32],
|
||||
epoch: u64,
|
||||
) -> Result<Vec<u8>> {
|
||||
let msgs = issuer_messages(role_id, epoch);
|
||||
let sig = BlindSignature::<CS>::blind_sign(
|
||||
&self.sk,
|
||||
&self.pk,
|
||||
Some(commitment_with_proof),
|
||||
Some(BBS_HEADER),
|
||||
Some(&msgs),
|
||||
)
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
Ok(sig.to_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl IssuerPublicKey {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.inner.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
let inner = BBSplusPublicKey::from_bytes(bytes)
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
pub(crate) fn inner(&self) -> &BBSplusPublicKey {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare a blind commitment hiding the member's secret.
|
||||
/// Returns the commitment (to send to the issuer) and the secret material
|
||||
/// (to keep locally for later proof generation).
|
||||
pub fn prepare_blind_request() -> Result<BlindRequest> {
|
||||
let member_secret = zkryptium::utils::util::bbsplus_utils::generate_random_secret(32);
|
||||
|
||||
let (commitment, prover_blind) =
|
||||
Commitment::<CS>::commit(Some(&[member_secret.clone()]))
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
|
||||
Ok(BlindRequest {
|
||||
commitment_with_proof: commitment.to_bytes(),
|
||||
prover_blind,
|
||||
member_secret,
|
||||
})
|
||||
}
|
||||
|
||||
impl Credential {
|
||||
/// Finalize a credential after receiving the blind signature from the issuer.
|
||||
pub fn finalize(
|
||||
blind_sig_bytes: &[u8],
|
||||
member_secret: Vec<u8>,
|
||||
prover_blind: BlindFactor,
|
||||
role_id: [u8; 32],
|
||||
epoch: u64,
|
||||
pk: &IssuerPublicKey,
|
||||
) -> Result<Self> {
|
||||
let sig_arr: [u8; 80] = blind_sig_bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CredentialError("blind signature must be 80 bytes".into()))?;
|
||||
|
||||
let blind_sig = BlindSignature::<CS>::from_bytes(&sig_arr)
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
|
||||
let msgs = issuer_messages(&role_id, epoch);
|
||||
blind_sig
|
||||
.verify_blind_sign(
|
||||
&pk.inner,
|
||||
Some(BBS_HEADER),
|
||||
Some(&msgs),
|
||||
Some(&[member_secret.clone()]),
|
||||
Some(&prover_blind),
|
||||
)
|
||||
.map_err(|e| Error::CredentialError(format!("credential verification failed: {e}")))?;
|
||||
|
||||
Ok(Self {
|
||||
blind_sig_bytes: sig_arr,
|
||||
member_secret,
|
||||
prover_blind,
|
||||
role_id,
|
||||
epoch,
|
||||
pk: pk.inner.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a ZK presentation bound to a session context (e.g. transcript hash).
|
||||
/// Reveals role_id and epoch; hides member_secret.
|
||||
pub fn present(&self, nonce: &[u8]) -> Result<Presentation> {
|
||||
let msgs = issuer_messages(&self.role_id, self.epoch);
|
||||
|
||||
let proof = PoKSignature::<CS>::blind_proof_gen(
|
||||
&self.pk,
|
||||
&self.blind_sig_bytes,
|
||||
Some(BBS_HEADER),
|
||||
Some(nonce),
|
||||
Some(&msgs),
|
||||
Some(&[self.member_secret.clone()]),
|
||||
Some(&[0, 1]), // disclose both issuer messages (role_id, epoch)
|
||||
None, // hide all committed messages (member_secret)
|
||||
Some(&self.prover_blind),
|
||||
)
|
||||
.map_err(|e| Error::CredentialError(format!("proof generation failed: {e}")))?;
|
||||
|
||||
Ok(Presentation {
|
||||
proof_bytes: proof.to_bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn role_id(&self) -> &[u8; 32] {
|
||||
&self.role_id
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> u64 {
|
||||
self.epoch
|
||||
}
|
||||
}
|
||||
|
||||
impl Presentation {
|
||||
pub fn to_bytes(&self) -> &[u8] {
|
||||
&self.proof_bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Self {
|
||||
Self { proof_bytes: bytes }
|
||||
}
|
||||
}
|
||||
|
||||
/// Server-side: verify a presentation against a public key, expected role_id and epoch.
|
||||
pub fn verify_presentation(
|
||||
pk: &IssuerPublicKey,
|
||||
presentation: &Presentation,
|
||||
role_id: &[u8; 32],
|
||||
epoch: u64,
|
||||
nonce: &[u8],
|
||||
) -> Result<()> {
|
||||
let proof = PoKSignature::<CS>::from_bytes(&presentation.proof_bytes)
|
||||
.map_err(|e| Error::CredentialError(format!("invalid proof: {e}")))?;
|
||||
|
||||
let disclosed_msgs = issuer_messages(role_id, epoch);
|
||||
|
||||
proof
|
||||
.blind_proof_verify(
|
||||
pk.inner(),
|
||||
Some(BBS_HEADER),
|
||||
Some(nonce),
|
||||
Some(2), // L = number of issuer messages
|
||||
Some(&disclosed_msgs),
|
||||
None, // no disclosed committed messages
|
||||
Some(&[0, 1]),
|
||||
None,
|
||||
)
|
||||
.map_err(|_| Error::InvalidPresentation)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn role_id_deterministic() {
|
||||
let id1 = role_id("admin");
|
||||
let id2 = role_id("admin");
|
||||
let id3 = role_id("reader");
|
||||
assert_eq!(id1, id2);
|
||||
assert_ne!(id1, id3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_blind_credential_flow() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = role_id("admin");
|
||||
let epoch = 1u64;
|
||||
|
||||
let request = prepare_blind_request().unwrap();
|
||||
|
||||
let blind_sig = issuer
|
||||
.issue_blind(&request.commitment_with_proof, &rid, epoch)
|
||||
.unwrap();
|
||||
|
||||
let cred = Credential::finalize(
|
||||
&blind_sig,
|
||||
request.member_secret,
|
||||
request.prover_blind,
|
||||
rid,
|
||||
epoch,
|
||||
&pk,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let nonce = b"session-transcript-hash-placeholder";
|
||||
let presentation = cred.present(nonce).unwrap();
|
||||
|
||||
verify_presentation(&pk, &presentation, &rid, epoch, nonce).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_role_rejected() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = role_id("admin");
|
||||
|
||||
let request = prepare_blind_request().unwrap();
|
||||
let blind_sig = issuer.issue_blind(&request.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&blind_sig, request.member_secret, request.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let nonce = b"nonce";
|
||||
let presentation = cred.present(nonce).unwrap();
|
||||
|
||||
let wrong_rid = role_id("reader");
|
||||
assert!(verify_presentation(&pk, &presentation, &wrong_rid, 1, nonce).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_epoch_rejected() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = role_id("admin");
|
||||
|
||||
let request = prepare_blind_request().unwrap();
|
||||
let blind_sig = issuer.issue_blind(&request.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&blind_sig, request.member_secret, request.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let nonce = b"nonce";
|
||||
let presentation = cred.present(nonce).unwrap();
|
||||
|
||||
assert!(verify_presentation(&pk, &presentation, &rid, 2, nonce).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_key_rejected() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = role_id("admin");
|
||||
|
||||
let request = prepare_blind_request().unwrap();
|
||||
let blind_sig = issuer.issue_blind(&request.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&blind_sig, request.member_secret, request.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let nonce = b"nonce";
|
||||
let presentation = cred.present(nonce).unwrap();
|
||||
|
||||
let other_issuer = IssuerKeyPair::generate().unwrap();
|
||||
let other_pk = other_issuer.public_key();
|
||||
assert!(verify_presentation(&other_pk, &presentation, &rid, 1, nonce).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presentations_are_unlinkable() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = role_id("admin");
|
||||
|
||||
let request = prepare_blind_request().unwrap();
|
||||
let blind_sig = issuer.issue_blind(&request.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&blind_sig, request.member_secret, request.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let p1 = cred.present(b"nonce1").unwrap();
|
||||
let p2 = cred.present(b"nonce2").unwrap();
|
||||
|
||||
assert_ne!(p1.to_bytes(), p2.to_bytes());
|
||||
|
||||
verify_presentation(&pk, &p1, &rid, 1, b"nonce1").unwrap();
|
||||
verify_presentation(&pk, &p2, &rid, 1, b"nonce2").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issuer_key_serialization() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let pk_bytes = pk.to_bytes();
|
||||
let pk2 = IssuerPublicKey::from_bytes(&pk_bytes).unwrap();
|
||||
assert_eq!(pk.to_bytes(), pk2.to_bytes());
|
||||
|
||||
let sk_bytes = issuer.secret_key_bytes();
|
||||
let issuer2 = IssuerKeyPair::from_secret_key_bytes(&sk_bytes).unwrap();
|
||||
assert_eq!(issuer2.public_key().to_bytes(), pk_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_members_same_role() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = role_id("editors");
|
||||
let nonce = b"session-nonce";
|
||||
|
||||
for _ in 0..3 {
|
||||
let req = prepare_blind_request().unwrap();
|
||||
let sig = issuer.issue_blind(&req.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&sig, req.member_secret, req.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
let pres = cred.present(nonce).unwrap();
|
||||
verify_presentation(&pk, &pres, &rid, 1, nonce).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/credential/mod.rs
Normal file
10
src/credential/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub mod bbs;
|
||||
pub mod roles;
|
||||
pub mod schnorr;
|
||||
|
||||
pub use bbs::{
|
||||
Credential, IssuerKeyPair, IssuerPublicKey, Presentation,
|
||||
prepare_blind_request, role_id, verify_presentation,
|
||||
};
|
||||
pub use roles::RoleRegistry;
|
||||
pub use schnorr::{Keypair, PublicKey};
|
||||
144
src/credential/roles.rs
Normal file
144
src/credential/roles.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::bbs::{self, IssuerPublicKey, Presentation};
|
||||
use crate::{Error, Result};
|
||||
|
||||
struct RoleEntry {
|
||||
public_key: IssuerPublicKey,
|
||||
current_epoch: u64,
|
||||
}
|
||||
|
||||
/// Server-side registry mapping opaque role IDs to BBS+ public keys.
|
||||
///
|
||||
/// The server stores only public keys and epochs — it knows nothing about
|
||||
/// role names, member identities, or credential contents. Verification
|
||||
/// uses the BBS+ public key; the server cannot forge credentials.
|
||||
pub struct RoleRegistry {
|
||||
roles: HashMap<[u8; 32], RoleEntry>,
|
||||
}
|
||||
|
||||
impl RoleRegistry {
|
||||
pub fn new() -> Self {
|
||||
RoleRegistry {
|
||||
roles: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a role's BBS+ public key. Called by the role creator
|
||||
/// (not the server itself) to announce a new role.
|
||||
pub fn register_role(&mut self, role_id: [u8; 32], pk: IssuerPublicKey, epoch: u64) {
|
||||
self.roles.insert(role_id, RoleEntry {
|
||||
public_key: pk,
|
||||
current_epoch: epoch,
|
||||
});
|
||||
}
|
||||
|
||||
/// Advance the epoch for a role. Credentials issued for older epochs
|
||||
/// will be rejected, enforcing revocation.
|
||||
pub fn set_epoch(&mut self, role_id: &[u8; 32], epoch: u64) -> Result<()> {
|
||||
let entry = self.roles.get_mut(role_id)
|
||||
.ok_or(Error::RoleNotRegistered)?;
|
||||
entry.current_epoch = epoch;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_epoch(&self, role_id: &[u8; 32]) -> Result<u64> {
|
||||
let entry = self.roles.get(role_id)
|
||||
.ok_or(Error::RoleNotRegistered)?;
|
||||
Ok(entry.current_epoch)
|
||||
}
|
||||
|
||||
pub fn get_public_key(&self, role_id: &[u8; 32]) -> Result<&IssuerPublicKey> {
|
||||
let entry = self.roles.get(role_id)
|
||||
.ok_or(Error::RoleNotRegistered)?;
|
||||
Ok(&entry.public_key)
|
||||
}
|
||||
|
||||
pub fn has_role(&self, role_id: &[u8; 32]) -> bool {
|
||||
self.roles.contains_key(role_id)
|
||||
}
|
||||
|
||||
/// Verify a BBS+ presentation against the stored public key and current epoch.
|
||||
pub fn verify_presentation(
|
||||
&self,
|
||||
role_id: &[u8; 32],
|
||||
presentation: &Presentation,
|
||||
nonce: &[u8],
|
||||
) -> Result<()> {
|
||||
let entry = self.roles.get(role_id)
|
||||
.ok_or(Error::RoleNotRegistered)?;
|
||||
|
||||
bbs::verify_presentation(
|
||||
&entry.public_key,
|
||||
presentation,
|
||||
role_id,
|
||||
entry.current_epoch,
|
||||
nonce,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RoleRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::credential::bbs::{self, IssuerKeyPair};
|
||||
|
||||
#[test]
|
||||
fn register_and_verify() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = bbs::role_id("admin");
|
||||
|
||||
let mut reg = RoleRegistry::new();
|
||||
reg.register_role(rid, pk.clone(), 1);
|
||||
|
||||
let req = bbs::prepare_blind_request().unwrap();
|
||||
let sig = issuer.issue_blind(&req.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = bbs::Credential::finalize(
|
||||
&sig, req.member_secret, req.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let nonce = b"test-nonce";
|
||||
let pres = cred.present(nonce).unwrap();
|
||||
reg.verify_presentation(&rid, &pres, nonce).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregistered_role_rejected() {
|
||||
let reg = RoleRegistry::new();
|
||||
let pres = bbs::Presentation::from_bytes(vec![0u8; 100]);
|
||||
let rid = bbs::role_id("unknown");
|
||||
assert!(reg.verify_presentation(&rid, &pres, b"n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_advance_revokes_old_credentials() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = bbs::role_id("ops");
|
||||
|
||||
let mut reg = RoleRegistry::new();
|
||||
reg.register_role(rid, pk.clone(), 1);
|
||||
|
||||
let req = bbs::prepare_blind_request().unwrap();
|
||||
let sig = issuer.issue_blind(&req.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = bbs::Credential::finalize(
|
||||
&sig, req.member_secret, req.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let nonce = b"n";
|
||||
let pres = cred.present(nonce).unwrap();
|
||||
reg.verify_presentation(&rid, &pres, nonce).unwrap();
|
||||
|
||||
reg.set_epoch(&rid, 2).unwrap();
|
||||
// Old credential (epoch 1) now fails against the registry expecting epoch 2
|
||||
let pres2 = cred.present(b"n2").unwrap();
|
||||
assert!(reg.verify_presentation(&rid, &pres2, b"n2").is_err());
|
||||
}
|
||||
}
|
||||
78
src/credential/schnorr.rs
Normal file
78
src/credential/schnorr.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use curve25519_dalek::{
|
||||
constants::RISTRETTO_BASEPOINT_TABLE,
|
||||
ristretto::{CompressedRistretto, RistrettoPoint},
|
||||
scalar::Scalar,
|
||||
};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct SecretKey {
|
||||
scalar: Scalar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PublicKey {
|
||||
pub(crate) point: RistrettoPoint,
|
||||
pub(crate) compressed: CompressedRistretto,
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Keypair {
|
||||
secret: SecretKey,
|
||||
#[zeroize(skip)]
|
||||
public: PublicKey,
|
||||
}
|
||||
|
||||
impl Keypair {
|
||||
pub fn generate<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
let point = &scalar * RISTRETTO_BASEPOINT_TABLE;
|
||||
Keypair {
|
||||
secret: SecretKey { scalar },
|
||||
public: PublicKey {
|
||||
point,
|
||||
compressed: point.compress(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public(&self) -> &PublicKey {
|
||||
&self.public
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.compressed.to_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: [u8; 32]) -> Result<Self> {
|
||||
let compressed = CompressedRistretto::from_slice(&bytes)
|
||||
.map_err(|_| Error::DeserializationError("invalid public key length"))?;
|
||||
let point = compressed
|
||||
.decompress()
|
||||
.ok_or(Error::DeserializationError("invalid ristretto point"))?;
|
||||
Ok(PublicKey { point, compressed })
|
||||
}
|
||||
|
||||
pub fn as_compressed(&self) -> &CompressedRistretto {
|
||||
&self.compressed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[test]
|
||||
fn pubkey_serialization() {
|
||||
let kp = Keypair::generate(&mut OsRng);
|
||||
let bytes = kp.public().to_bytes();
|
||||
let pk2 = PublicKey::from_bytes(bytes).unwrap();
|
||||
assert_eq!(*kp.public(), pk2);
|
||||
}
|
||||
}
|
||||
33
src/error.rs
Normal file
33
src/error.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("replay detected")]
|
||||
ReplayDetected,
|
||||
|
||||
#[error("handshake error: {0}")]
|
||||
HandshakeError(&'static str),
|
||||
|
||||
#[error("decryption failed")]
|
||||
DecryptionFailed,
|
||||
|
||||
#[error("invalid packet: {0}")]
|
||||
InvalidPacket(&'static str),
|
||||
|
||||
#[error("deserialization error: {0}")]
|
||||
DeserializationError(&'static str),
|
||||
|
||||
#[error("session not established")]
|
||||
SessionNotEstablished,
|
||||
|
||||
#[error("BBS+ credential error: {0}")]
|
||||
CredentialError(String),
|
||||
|
||||
#[error("invalid presentation")]
|
||||
InvalidPresentation,
|
||||
|
||||
#[error("role not registered")]
|
||||
RoleNotRegistered,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
10
src/lib.rs
Normal file
10
src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub mod credential;
|
||||
pub mod error;
|
||||
pub mod node;
|
||||
pub mod transport;
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
mod python;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use node::{Node, PendingConnect, MAX_BBS_AUTH_PROOF_BYTES};
|
||||
201
src/node.rs
Normal file
201
src/node.rs
Normal file
@ -0,0 +1,201 @@
|
||||
//! High-level client/server node: X25519 transport + BBS+ credential authentication.
|
||||
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::credential::{Keypair, PublicKey, RoleRegistry};
|
||||
use crate::credential::bbs::{Credential, Presentation};
|
||||
use crate::transport::handshake::{self, InitiatorHandshake, HANDSHAKE_MSG_LEN};
|
||||
use crate::transport::Session;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Maximum BBS+ proof size accepted in an auth packet (DoS bound).
|
||||
pub const MAX_BBS_AUTH_PROOF_BYTES: usize = 256 * 1024;
|
||||
|
||||
/// A node that can act as either client (initiator) or server (responder).
|
||||
/// Holds the node's long-term identity keypair used for the transport handshake.
|
||||
pub struct Node {
|
||||
keypair: Keypair,
|
||||
}
|
||||
|
||||
/// Pending connection state on the initiator side.
|
||||
pub struct PendingConnect {
|
||||
handshake: InitiatorHandshake,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new(keypair: Keypair) -> Self {
|
||||
Node { keypair }
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
self.keypair.public()
|
||||
}
|
||||
|
||||
// ── Initiator (client) side ──────────────────────────────────────
|
||||
|
||||
/// Begin a connection. Returns the pending state and the handshake
|
||||
/// message to send to the responder.
|
||||
pub fn connect<R: CryptoRng + RngCore>(
|
||||
&self,
|
||||
rng: R,
|
||||
) -> (PendingConnect, [u8; HANDSHAKE_MSG_LEN]) {
|
||||
let (hs, msg) = InitiatorHandshake::begin(rng);
|
||||
(PendingConnect { handshake: hs }, msg)
|
||||
}
|
||||
|
||||
/// Complete the connection using a BBS+ credential.
|
||||
/// Produces an encrypted session and an auth packet containing a
|
||||
/// ZK presentation bound to the session transcript.
|
||||
///
|
||||
/// Auth payload layout: `[role_id: 32] [epoch: 8 LE] [proof_len: u32 LE] [proof]`
|
||||
/// (`epoch` is redundant with the credential; kept for forward compatibility.)
|
||||
pub fn complete_connect(
|
||||
&self,
|
||||
pending: PendingConnect,
|
||||
response_msg: &[u8; HANDSHAKE_MSG_LEN],
|
||||
credential: &Credential,
|
||||
) -> Result<(Session, Vec<u8>)> {
|
||||
let mut session = pending.handshake.complete(response_msg)?;
|
||||
|
||||
let transcript = session.transcript_hash();
|
||||
let presentation = credential.present(transcript)?;
|
||||
let proof_bytes = presentation.to_bytes();
|
||||
|
||||
if proof_bytes.len() > MAX_BBS_AUTH_PROOF_BYTES {
|
||||
return Err(Error::InvalidPacket("bbs auth proof exceeds maximum size"));
|
||||
}
|
||||
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(credential.role_id());
|
||||
payload.extend_from_slice(&credential.epoch().to_le_bytes());
|
||||
payload.extend_from_slice(&(proof_bytes.len() as u32).to_le_bytes());
|
||||
payload.extend_from_slice(proof_bytes);
|
||||
|
||||
let encrypted_auth = session.encrypt(&payload)?;
|
||||
Ok((session, encrypted_auth))
|
||||
}
|
||||
|
||||
// ── Responder (server) side ──────────────────────────────────────
|
||||
|
||||
/// Accept an incoming handshake initiation.
|
||||
/// Returns a session and the response message to send back.
|
||||
pub fn accept<R: CryptoRng + RngCore>(
|
||||
&self,
|
||||
rng: R,
|
||||
init_msg: &[u8; HANDSHAKE_MSG_LEN],
|
||||
) -> Result<(Session, [u8; HANDSHAKE_MSG_LEN])> {
|
||||
let resp = handshake::respond(rng, init_msg)?;
|
||||
Ok((resp.session, resp.response_msg))
|
||||
}
|
||||
|
||||
/// Verify the initiator's encrypted BBS+ auth packet.
|
||||
/// Returns the authenticated opaque `role_id` (32 bytes).
|
||||
pub fn verify_auth(
|
||||
&self,
|
||||
session: &mut Session,
|
||||
encrypted_auth: &[u8],
|
||||
registry: &RoleRegistry,
|
||||
) -> Result<[u8; 32]> {
|
||||
let payload = session.decrypt(encrypted_auth)?;
|
||||
let transcript = *session.transcript_hash();
|
||||
|
||||
// [role_id: 32] [epoch: 8] [proof_len: 4] [proof: ...]
|
||||
if payload.len() < 44 {
|
||||
return Err(Error::InvalidPacket("bbs auth payload too short"));
|
||||
}
|
||||
|
||||
let mut role_id = [0u8; 32];
|
||||
role_id.copy_from_slice(&payload[..32]);
|
||||
|
||||
let proof_len = u32::from_le_bytes(payload[40..44].try_into().unwrap()) as usize;
|
||||
if proof_len > MAX_BBS_AUTH_PROOF_BYTES {
|
||||
return Err(Error::InvalidPacket("bbs auth proof length exceeds maximum"));
|
||||
}
|
||||
if payload.len() < 44 + proof_len {
|
||||
return Err(Error::InvalidPacket("bbs auth: proof truncated"));
|
||||
}
|
||||
|
||||
let presentation = Presentation::from_bytes(payload[44..44 + proof_len].to_vec());
|
||||
registry.verify_presentation(&role_id, &presentation, &transcript)?;
|
||||
|
||||
Ok(role_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::credential::bbs::{self, IssuerKeyPair};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[test]
|
||||
fn full_handshake_with_bbs_auth() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = bbs::role_id("admin");
|
||||
|
||||
let mut registry = RoleRegistry::new();
|
||||
registry.register_role(rid, pk.clone(), 1);
|
||||
|
||||
let req = bbs::prepare_blind_request().unwrap();
|
||||
let sig = issuer.issue_blind(&req.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&sig, req.member_secret, req.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let client_kp = Keypair::generate(&mut OsRng);
|
||||
let server_kp = Keypair::generate(&mut OsRng);
|
||||
let client = Node::new(client_kp);
|
||||
let server = Node::new(server_kp);
|
||||
|
||||
let (pending, init_msg) = client.connect(OsRng);
|
||||
let (mut server_session, response_msg) = server.accept(OsRng, &init_msg).unwrap();
|
||||
let (mut client_session, auth_packet) = client
|
||||
.complete_connect(pending, &response_msg, &cred)
|
||||
.unwrap();
|
||||
|
||||
let role_id = server
|
||||
.verify_auth(&mut server_session, &auth_packet, ®istry)
|
||||
.unwrap();
|
||||
assert_eq!(role_id, rid);
|
||||
|
||||
let pkt = client_session.encrypt(b"admin command").unwrap();
|
||||
assert_eq!(server_session.decrypt(&pkt).unwrap(), b"admin command");
|
||||
|
||||
let pkt = server_session.encrypt(b"response").unwrap();
|
||||
assert_eq!(client_session.decrypt(&pkt).unwrap(), b"response");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bbs_wrong_role_rejected() {
|
||||
let issuer = IssuerKeyPair::generate().unwrap();
|
||||
let pk = issuer.public_key();
|
||||
let rid = bbs::role_id("reader");
|
||||
|
||||
let mut registry = RoleRegistry::new();
|
||||
let admin_rid = bbs::role_id("admin");
|
||||
let admin_issuer = IssuerKeyPair::generate().unwrap();
|
||||
registry.register_role(admin_rid, admin_issuer.public_key(), 1);
|
||||
registry.register_role(rid, pk.clone(), 1);
|
||||
|
||||
let req = bbs::prepare_blind_request().unwrap();
|
||||
let sig = issuer.issue_blind(&req.commitment_with_proof, &rid, 1).unwrap();
|
||||
let cred = Credential::finalize(
|
||||
&sig, req.member_secret, req.prover_blind, rid, 1, &pk,
|
||||
).unwrap();
|
||||
|
||||
let client = Node::new(Keypair::generate(&mut OsRng));
|
||||
let server = Node::new(Keypair::generate(&mut OsRng));
|
||||
|
||||
let (pending, init_msg) = client.connect(OsRng);
|
||||
let (mut server_session, response_msg) = server.accept(OsRng, &init_msg).unwrap();
|
||||
let (_, auth_packet) = client
|
||||
.complete_connect(pending, &response_msg, &cred)
|
||||
.unwrap();
|
||||
|
||||
let verified_rid = server
|
||||
.verify_auth(&mut server_session, &auth_packet, ®istry)
|
||||
.unwrap();
|
||||
assert_eq!(verified_rid, rid);
|
||||
}
|
||||
}
|
||||
444
src/python.rs
Normal file
444
src/python.rs
Normal file
@ -0,0 +1,444 @@
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyBytes;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::credential::{self, bbs};
|
||||
use crate::transport::handshake::HANDSHAKE_MSG_LEN;
|
||||
|
||||
fn to_py_err(e: crate::Error) -> PyErr {
|
||||
PyValueError::new_err(e.to_string())
|
||||
}
|
||||
|
||||
fn bytes_to_hex(b: &[u8]) -> String {
|
||||
b.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
// ── Ristretto Keypair (transport identity) ───────────────────────────
|
||||
|
||||
#[pyclass(name = "Keypair")]
|
||||
pub struct PyKeypair {
|
||||
inner: Option<credential::Keypair>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKeypair {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
PyKeypair {
|
||||
inner: Some(credential::Keypair::generate(&mut OsRng)),
|
||||
}
|
||||
}
|
||||
|
||||
fn public_key(&self) -> PyResult<PyPublicKey> {
|
||||
let kp = self.inner.as_ref().ok_or_else(|| {
|
||||
PyValueError::new_err("keypair was consumed by Node")
|
||||
})?;
|
||||
Ok(PyPublicKey {
|
||||
inner: *kp.public(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ristretto PublicKey ──────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "PublicKey")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyPublicKey {
|
||||
inner: credential::PublicKey,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPublicKey {
|
||||
fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, &self.inner.to_bytes())
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn from_bytes(bytes: &[u8]) -> PyResult<Self> {
|
||||
if bytes.len() != 32 {
|
||||
return Err(PyValueError::new_err("public key must be 32 bytes"));
|
||||
}
|
||||
let arr: [u8; 32] = bytes.try_into().unwrap();
|
||||
Ok(PyPublicKey {
|
||||
inner: credential::PublicKey::from_bytes(arr).map_err(to_py_err)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn __eq__(&self, other: &PyPublicKey) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
let b = self.inner.to_bytes();
|
||||
u64::from_le_bytes(b[..8].try_into().unwrap())
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
let hex = bytes_to_hex(&self.inner.to_bytes());
|
||||
format!("PublicKey({hex})")
|
||||
}
|
||||
}
|
||||
|
||||
// ── BBS+ Issuer ──────────────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "BbsIssuer")]
|
||||
pub struct PyBbsIssuer {
|
||||
inner: bbs::IssuerKeyPair,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBbsIssuer {
|
||||
#[new]
|
||||
fn new() -> PyResult<Self> {
|
||||
let inner = bbs::IssuerKeyPair::generate().map_err(to_py_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn from_secret_key(bytes: &[u8]) -> PyResult<Self> {
|
||||
let inner = bbs::IssuerKeyPair::from_secret_key_bytes(bytes).map_err(to_py_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
fn public_key(&self) -> PyBbsPublicKey {
|
||||
PyBbsPublicKey { inner: self.inner.public_key() }
|
||||
}
|
||||
|
||||
fn secret_key_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, &self.inner.secret_key_bytes())
|
||||
}
|
||||
|
||||
fn issue_blind<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
commitment_with_proof: &[u8],
|
||||
role_id: &[u8],
|
||||
epoch: u64,
|
||||
) -> PyResult<Bound<'py, PyBytes>> {
|
||||
if role_id.len() != 32 {
|
||||
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
||||
}
|
||||
let mut rid = [0u8; 32];
|
||||
rid.copy_from_slice(role_id);
|
||||
let sig = self.inner.issue_blind(commitment_with_proof, &rid, epoch).map_err(to_py_err)?;
|
||||
Ok(PyBytes::new(py, &sig))
|
||||
}
|
||||
}
|
||||
|
||||
// ── BBS+ Public Key ──────────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "BbsPublicKey")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyBbsPublicKey {
|
||||
inner: bbs::IssuerPublicKey,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBbsPublicKey {
|
||||
fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, &self.inner.to_bytes())
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn from_bytes(bytes: &[u8]) -> PyResult<Self> {
|
||||
let inner = bbs::IssuerPublicKey::from_bytes(bytes).map_err(to_py_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
// ── BBS+ Blind Request ──────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "BlindRequest")]
|
||||
pub struct PyBlindRequest {
|
||||
commitment_with_proof: Vec<u8>,
|
||||
prover_blind_bytes: Vec<u8>,
|
||||
member_secret: Vec<u8>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBlindRequest {
|
||||
fn commitment_with_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, &self.commitment_with_proof)
|
||||
}
|
||||
|
||||
fn prover_blind<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, &self.prover_blind_bytes)
|
||||
}
|
||||
|
||||
fn member_secret<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, &self.member_secret)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn prepare_blind_request() -> PyResult<PyBlindRequest> {
|
||||
let req = bbs::prepare_blind_request().map_err(to_py_err)?;
|
||||
Ok(PyBlindRequest {
|
||||
commitment_with_proof: req.commitment_with_proof,
|
||||
prover_blind_bytes: req.prover_blind.to_bytes().to_vec(),
|
||||
member_secret: req.member_secret,
|
||||
})
|
||||
}
|
||||
|
||||
// ── BBS+ Credential ─────────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "Credential")]
|
||||
pub struct PyCredential {
|
||||
inner: bbs::Credential,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCredential {
|
||||
#[staticmethod]
|
||||
fn finalize(
|
||||
blind_sig_bytes: &[u8],
|
||||
member_secret: &[u8],
|
||||
prover_blind: &[u8],
|
||||
role_id: &[u8],
|
||||
epoch: u64,
|
||||
pk: &PyBbsPublicKey,
|
||||
) -> PyResult<Self> {
|
||||
if role_id.len() != 32 {
|
||||
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
||||
}
|
||||
if prover_blind.len() != 32 {
|
||||
return Err(PyValueError::new_err("prover_blind must be 32 bytes"));
|
||||
}
|
||||
let mut rid = [0u8; 32];
|
||||
rid.copy_from_slice(role_id);
|
||||
|
||||
let pb_arr: [u8; 32] = prover_blind.try_into().unwrap();
|
||||
let blind_factor = zkryptium::bbsplus::commitment::BlindFactor::from_bytes(&pb_arr)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
|
||||
let inner = bbs::Credential::finalize(
|
||||
blind_sig_bytes,
|
||||
member_secret.to_vec(),
|
||||
blind_factor,
|
||||
rid,
|
||||
epoch,
|
||||
&pk.inner,
|
||||
).map_err(to_py_err)?;
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
fn present<'py>(&self, py: Python<'py>, nonce: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
||||
let pres = self.inner.present(nonce).map_err(to_py_err)?;
|
||||
Ok(PyBytes::new(py, pres.to_bytes()))
|
||||
}
|
||||
|
||||
fn role_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, self.inner.role_id())
|
||||
}
|
||||
|
||||
fn epoch(&self) -> u64 {
|
||||
self.inner.epoch()
|
||||
}
|
||||
}
|
||||
|
||||
// ── BBS+ Role ID helper ─────────────────────────────────────────────
|
||||
|
||||
#[pyfunction]
|
||||
fn role_id<'py>(py: Python<'py>, name: &str) -> Bound<'py, PyBytes> {
|
||||
let rid = bbs::role_id(name);
|
||||
PyBytes::new(py, &rid)
|
||||
}
|
||||
|
||||
// ── RoleRegistry (BBS+ server-side) ──────────────────────────────────
|
||||
|
||||
#[pyclass(name = "RoleRegistry")]
|
||||
pub struct PyRoleRegistry {
|
||||
inner: credential::RoleRegistry,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRoleRegistry {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
PyRoleRegistry {
|
||||
inner: credential::RoleRegistry::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_role(&mut self, role_id: &[u8], pk: &PyBbsPublicKey, epoch: u64) -> PyResult<()> {
|
||||
if role_id.len() != 32 {
|
||||
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
||||
}
|
||||
let mut rid = [0u8; 32];
|
||||
rid.copy_from_slice(role_id);
|
||||
self.inner.register_role(rid, pk.inner.clone(), epoch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_epoch(&mut self, role_id: &[u8], epoch: u64) -> PyResult<()> {
|
||||
if role_id.len() != 32 {
|
||||
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
||||
}
|
||||
let mut rid = [0u8; 32];
|
||||
rid.copy_from_slice(role_id);
|
||||
self.inner.set_epoch(&rid, epoch).map_err(to_py_err)
|
||||
}
|
||||
|
||||
fn verify_presentation(&self, role_id: &[u8], proof_bytes: &[u8], nonce: &[u8]) -> PyResult<bool> {
|
||||
if role_id.len() != 32 {
|
||||
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
||||
}
|
||||
let mut rid = [0u8; 32];
|
||||
rid.copy_from_slice(role_id);
|
||||
let pres = bbs::Presentation::from_bytes(proof_bytes.to_vec());
|
||||
match self.inner.verify_presentation(&rid, &pres, nonce) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(crate::Error::InvalidPresentation | crate::Error::RoleNotRegistered) => Ok(false),
|
||||
Err(e) => Err(to_py_err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_role(&self, role_id: &[u8]) -> PyResult<bool> {
|
||||
if role_id.len() != 32 {
|
||||
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
||||
}
|
||||
let mut rid = [0u8; 32];
|
||||
rid.copy_from_slice(role_id);
|
||||
Ok(self.inner.has_role(&rid))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Session ──────────────────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "Session")]
|
||||
pub struct PySession {
|
||||
inner: crate::transport::Session,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySession {
|
||||
fn transcript_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
||||
PyBytes::new(py, self.inner.transcript_hash())
|
||||
}
|
||||
|
||||
fn encrypt<'py>(&mut self, py: Python<'py>, plaintext: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
||||
let packet = self.inner.encrypt(plaintext).map_err(to_py_err)?;
|
||||
Ok(PyBytes::new(py, &packet))
|
||||
}
|
||||
|
||||
fn decrypt<'py>(&mut self, py: Python<'py>, packet: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
||||
let plaintext = self.inner.decrypt(packet).map_err(to_py_err)?;
|
||||
Ok(PyBytes::new(py, &plaintext))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Node (high-level API) ────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "Node")]
|
||||
pub struct PyNode {
|
||||
inner: crate::node::Node,
|
||||
}
|
||||
|
||||
#[pyclass(name = "PendingConnect")]
|
||||
pub struct PyPendingConnect {
|
||||
inner: Option<crate::node::PendingConnect>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyNode {
|
||||
#[new]
|
||||
fn new(keypair: &mut PyKeypair) -> PyResult<Self> {
|
||||
let kp = keypair.inner.take().ok_or_else(|| {
|
||||
PyValueError::new_err("keypair already consumed")
|
||||
})?;
|
||||
Ok(PyNode {
|
||||
inner: crate::node::Node::new(kp),
|
||||
})
|
||||
}
|
||||
|
||||
fn public_key(&self) -> PyPublicKey {
|
||||
PyPublicKey {
|
||||
inner: *self.inner.public_key(),
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&self) -> (PyPendingConnect, Vec<u8>) {
|
||||
let (pending, msg) = self.inner.connect(OsRng);
|
||||
(
|
||||
PyPendingConnect {
|
||||
inner: Some(pending),
|
||||
},
|
||||
msg.to_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Complete handshake with BBS+ credential authentication.
|
||||
fn complete_connect(
|
||||
&self,
|
||||
pending: &mut PyPendingConnect,
|
||||
response_msg: &[u8],
|
||||
credential: &PyCredential,
|
||||
) -> PyResult<(PySession, Vec<u8>)> {
|
||||
let p = pending
|
||||
.inner
|
||||
.take()
|
||||
.ok_or_else(|| PyValueError::new_err("PendingConnect already consumed"))?;
|
||||
|
||||
if response_msg.len() != HANDSHAKE_MSG_LEN {
|
||||
return Err(PyValueError::new_err("response_msg must be 32 bytes"));
|
||||
}
|
||||
let msg: [u8; HANDSHAKE_MSG_LEN] = response_msg.try_into().unwrap();
|
||||
|
||||
let (session, auth_packet) = self
|
||||
.inner
|
||||
.complete_connect(p, &msg, &credential.inner)
|
||||
.map_err(to_py_err)?;
|
||||
|
||||
Ok((PySession { inner: session }, auth_packet))
|
||||
}
|
||||
|
||||
fn accept(&self, init_msg: &[u8]) -> PyResult<(PySession, Vec<u8>)> {
|
||||
if init_msg.len() != HANDSHAKE_MSG_LEN {
|
||||
return Err(PyValueError::new_err("init_msg must be 32 bytes"));
|
||||
}
|
||||
let msg: [u8; HANDSHAKE_MSG_LEN] = init_msg.try_into().unwrap();
|
||||
let (session, response) = self.inner.accept(OsRng, &msg).map_err(to_py_err)?;
|
||||
Ok((PySession { inner: session }, response.to_vec()))
|
||||
}
|
||||
|
||||
/// Verify encrypted BBS+ auth packet. Returns the role_id (32 bytes) on success.
|
||||
fn verify_auth<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
session: &mut PySession,
|
||||
encrypted_auth: &[u8],
|
||||
registry: &PyRoleRegistry,
|
||||
) -> PyResult<Bound<'py, PyBytes>> {
|
||||
let rid = self
|
||||
.inner
|
||||
.verify_auth(&mut session.inner, encrypted_auth, ®istry.inner)
|
||||
.map_err(to_py_err)?;
|
||||
Ok(PyBytes::new(py, &rid))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Module ───────────────────────────────────────────────────────────
|
||||
|
||||
#[pymodule]
|
||||
fn _zkac(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add("MAX_BBS_AUTH_PROOF_BYTES", crate::node::MAX_BBS_AUTH_PROOF_BYTES)?;
|
||||
// Transport identity (ristretto255)
|
||||
m.add_class::<PyKeypair>()?;
|
||||
m.add_class::<PyPublicKey>()?;
|
||||
// BBS+ anonymous credentials
|
||||
m.add_class::<PyBbsIssuer>()?;
|
||||
m.add_class::<PyBbsPublicKey>()?;
|
||||
m.add_class::<PyBlindRequest>()?;
|
||||
m.add_class::<PyCredential>()?;
|
||||
m.add_function(wrap_pyfunction!(prepare_blind_request, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(role_id, m)?)?;
|
||||
// Server registry
|
||||
m.add_class::<PyRoleRegistry>()?;
|
||||
// Transport
|
||||
m.add_class::<PySession>()?;
|
||||
m.add_class::<PyNode>()?;
|
||||
m.add_class::<PyPendingConnect>()?;
|
||||
Ok(())
|
||||
}
|
||||
162
src/transport/handshake.rs
Normal file
162
src/transport/handshake.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use blake2::Blake2b512;
|
||||
use digest::Digest;
|
||||
use hkdf::Hkdf;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use sha2::Sha256;
|
||||
use subtle::ConstantTimeEq;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey as X25519Public, SharedSecret};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{Error, Result};
|
||||
use super::session::Session;
|
||||
|
||||
const TRANSCRIPT_DOMAIN: &[u8] = b"zkac-handshake-transcript-v1";
|
||||
const HKDF_INITIATOR_SEND: &[u8] = b"zkac-initiator-send-v1";
|
||||
const HKDF_RESPONDER_SEND: &[u8] = b"zkac-responder-send-v1";
|
||||
|
||||
/// Size of a handshake message (one X25519 ephemeral public key).
|
||||
pub const HANDSHAKE_MSG_LEN: usize = 32;
|
||||
|
||||
/// Initiator's handshake state, holding the ephemeral secret until
|
||||
/// the responder replies. Consumed on completion.
|
||||
pub struct InitiatorHandshake {
|
||||
ephemeral_secret: EphemeralSecret,
|
||||
our_public: X25519Public,
|
||||
}
|
||||
|
||||
/// Responder's handshake output.
|
||||
pub struct ResponderHandshake {
|
||||
pub session: Session,
|
||||
pub response_msg: [u8; HANDSHAKE_MSG_LEN],
|
||||
}
|
||||
|
||||
impl InitiatorHandshake {
|
||||
/// Begin a handshake. Returns `(state, message_to_send)`.
|
||||
pub fn begin<R: CryptoRng + RngCore>(rng: R) -> (Self, [u8; HANDSHAKE_MSG_LEN]) {
|
||||
let secret = EphemeralSecret::random_from_rng(rng);
|
||||
let public = X25519Public::from(&secret);
|
||||
let msg = public.to_bytes();
|
||||
let state = InitiatorHandshake {
|
||||
ephemeral_secret: secret,
|
||||
our_public: public,
|
||||
};
|
||||
(state, msg)
|
||||
}
|
||||
|
||||
/// Complete the handshake upon receiving the responder's ephemeral key.
|
||||
/// Consumes self (the ephemeral secret is used exactly once).
|
||||
pub fn complete(self, response_msg: &[u8; HANDSHAKE_MSG_LEN]) -> Result<Session> {
|
||||
let their_public = X25519Public::from(*response_msg);
|
||||
let shared = self.ephemeral_secret.diffie_hellman(&their_public);
|
||||
|
||||
if is_zero(shared.as_bytes()) {
|
||||
return Err(Error::HandshakeError("DH produced zero shared secret"));
|
||||
}
|
||||
|
||||
let transcript = compute_transcript(&self.our_public, &their_public);
|
||||
Ok(derive_session(&shared, &transcript, true))
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an incoming handshake initiation and produce a session + response.
|
||||
pub fn respond<R: CryptoRng + RngCore>(
|
||||
rng: R,
|
||||
init_msg: &[u8; HANDSHAKE_MSG_LEN],
|
||||
) -> Result<ResponderHandshake> {
|
||||
let their_public = X25519Public::from(*init_msg);
|
||||
let our_secret = EphemeralSecret::random_from_rng(rng);
|
||||
let our_public = X25519Public::from(&our_secret);
|
||||
let shared = our_secret.diffie_hellman(&their_public);
|
||||
|
||||
if is_zero(shared.as_bytes()) {
|
||||
return Err(Error::HandshakeError("DH produced zero shared secret"));
|
||||
}
|
||||
|
||||
let transcript = compute_transcript(&their_public, &our_public);
|
||||
let session = derive_session(&shared, &transcript, false);
|
||||
|
||||
Ok(ResponderHandshake {
|
||||
session,
|
||||
response_msg: our_public.to_bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Transcript binds both ephemeral keys to prevent MitM.
|
||||
fn compute_transcript(initiator_pub: &X25519Public, responder_pub: &X25519Public) -> [u8; 64] {
|
||||
let mut h = Blake2b512::new();
|
||||
h.update(TRANSCRIPT_DOMAIN);
|
||||
h.update(initiator_pub.as_bytes());
|
||||
h.update(responder_pub.as_bytes());
|
||||
h.finalize().into()
|
||||
}
|
||||
|
||||
/// Derive a bidirectional session from the shared secret and transcript.
|
||||
fn derive_session(shared: &SharedSecret, transcript: &[u8; 64], is_initiator: bool) -> Session {
|
||||
// Use the full 64-byte transcript as HKDF salt (no truncation).
|
||||
let hk = Hkdf::<Sha256>::new(Some(transcript.as_slice()), shared.as_bytes());
|
||||
|
||||
let mut i2r_key = [0u8; 32];
|
||||
let mut r2i_key = [0u8; 32];
|
||||
hk.expand(HKDF_INITIATOR_SEND, &mut i2r_key)
|
||||
.expect("HKDF expand failed — output length is valid");
|
||||
hk.expand(HKDF_RESPONDER_SEND, &mut r2i_key)
|
||||
.expect("HKDF expand failed — output length is valid");
|
||||
|
||||
let (send_key, recv_key) = if is_initiator {
|
||||
(i2r_key, r2i_key)
|
||||
} else {
|
||||
(r2i_key, i2r_key)
|
||||
};
|
||||
|
||||
// Derive a 32-byte session transcript hash for auth binding.
|
||||
let mut transcript_hash = [0u8; 32];
|
||||
hk.expand(b"zkac-transcript-hash-v1", &mut transcript_hash)
|
||||
.expect("HKDF expand failed — output length is valid");
|
||||
|
||||
let session = Session::new(send_key, recv_key, transcript_hash);
|
||||
|
||||
i2r_key.zeroize();
|
||||
r2i_key.zeroize();
|
||||
|
||||
session
|
||||
}
|
||||
|
||||
fn is_zero(bytes: &[u8; 32]) -> bool {
|
||||
bytes.ct_eq(&[0u8; 32]).into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[test]
|
||||
fn handshake_produces_matching_sessions() {
|
||||
let (initiator, init_msg) = InitiatorHandshake::begin(OsRng);
|
||||
let resp = respond(OsRng, &init_msg).unwrap();
|
||||
let init_session = initiator.complete(&resp.response_msg).unwrap();
|
||||
|
||||
// Both sides should have the same transcript hash
|
||||
assert_eq!(
|
||||
init_session.transcript_hash(),
|
||||
resp.session.transcript_hash()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_roundtrip_after_handshake() {
|
||||
let (initiator, init_msg) = InitiatorHandshake::begin(OsRng);
|
||||
let mut resp = respond(OsRng, &init_msg).unwrap();
|
||||
let mut init_session = initiator.complete(&resp.response_msg).unwrap();
|
||||
|
||||
// Initiator sends to responder
|
||||
let packet = init_session.encrypt(b"hello from initiator").unwrap();
|
||||
let plaintext = resp.session.decrypt(&packet).unwrap();
|
||||
assert_eq!(&plaintext, b"hello from initiator");
|
||||
|
||||
// Responder sends to initiator
|
||||
let packet = resp.session.encrypt(b"hello from responder").unwrap();
|
||||
let plaintext = init_session.decrypt(&packet).unwrap();
|
||||
assert_eq!(&plaintext, b"hello from responder");
|
||||
}
|
||||
}
|
||||
8
src/transport/mod.rs
Normal file
8
src/transport/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod handshake;
|
||||
pub mod packet;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
|
||||
pub use handshake::{InitiatorHandshake, ResponderHandshake};
|
||||
pub use replay::ReplayGuard;
|
||||
pub use session::Session;
|
||||
56
src/transport/packet.rs
Normal file
56
src/transport/packet.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Minimum wire packet: 8-byte counter + 16-byte AEAD tag.
|
||||
pub const MIN_WIRE_LEN: usize = 24;
|
||||
|
||||
/// A parsed wire packet. The session module handles crypto;
|
||||
/// this module handles serialization only.
|
||||
#[derive(Debug)]
|
||||
pub struct WirePacket<'a> {
|
||||
pub counter: u64,
|
||||
pub ciphertext: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> WirePacket<'a> {
|
||||
/// Parse raw bytes into counter + ciphertext.
|
||||
pub fn parse(data: &'a [u8]) -> Result<Self> {
|
||||
if data.len() < MIN_WIRE_LEN {
|
||||
return Err(Error::InvalidPacket("packet too short"));
|
||||
}
|
||||
|
||||
let counter = u64::from_le_bytes(data[..8].try_into().unwrap());
|
||||
let ciphertext = &data[8..];
|
||||
|
||||
Ok(WirePacket {
|
||||
counter,
|
||||
ciphertext,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a wire packet from counter and ciphertext (including AEAD tag).
|
||||
pub fn build(counter: u64, ciphertext: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(8 + ciphertext.len());
|
||||
out.extend_from_slice(&counter.to_le_bytes());
|
||||
out.extend_from_slice(ciphertext);
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn build_and_parse() {
|
||||
let ct = vec![0xAA; 32];
|
||||
let packet = build(42, &ct);
|
||||
let parsed = WirePacket::parse(&packet).unwrap();
|
||||
assert_eq!(parsed.counter, 42);
|
||||
assert_eq!(parsed.ciphertext, &ct[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_short_rejected() {
|
||||
assert!(WirePacket::parse(&[0u8; 10]).is_err());
|
||||
}
|
||||
}
|
||||
185
src/transport/replay.rs
Normal file
185
src/transport/replay.rs
Normal file
@ -0,0 +1,185 @@
|
||||
use crate::{Error, Result};
|
||||
|
||||
const WINDOW_SIZE: u64 = 2048;
|
||||
const BITMAP_WORDS: usize = (WINDOW_SIZE / 64) as usize;
|
||||
|
||||
/// Sliding-window replay guard (WireGuard-style).
|
||||
///
|
||||
/// Tracks seen counters and rejects duplicates or counters that
|
||||
/// fall behind the window. Window size is 2048 packets.
|
||||
///
|
||||
/// Uses a two-phase API: `check` validates without committing, then
|
||||
/// `accept` commits after the caller verifies AEAD integrity. This
|
||||
/// prevents counter-burning attacks where an attacker injects packets
|
||||
/// with valid counters but garbage ciphertext to exhaust the window.
|
||||
pub struct ReplayGuard {
|
||||
top: u64,
|
||||
bitmap: [u64; BITMAP_WORDS],
|
||||
}
|
||||
|
||||
impl ReplayGuard {
|
||||
pub fn new() -> Self {
|
||||
ReplayGuard {
|
||||
top: 0,
|
||||
bitmap: [0u64; BITMAP_WORDS],
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that a counter is fresh (not seen, not too old) without
|
||||
/// marking it as seen. Call `accept` after AEAD verification succeeds.
|
||||
pub fn check(&self, counter: u64) -> Result<()> {
|
||||
if counter == 0 {
|
||||
return Err(Error::ReplayDetected);
|
||||
}
|
||||
|
||||
if self.top == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if counter > self.top {
|
||||
Ok(())
|
||||
} else if self.top - counter >= WINDOW_SIZE {
|
||||
Err(Error::ReplayDetected)
|
||||
} else if self.is_set(counter) {
|
||||
Err(Error::ReplayDetected)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a counter as seen. Only call after AEAD decryption succeeds.
|
||||
pub fn accept(&mut self, counter: u64) {
|
||||
if self.top == 0 || counter > self.top {
|
||||
let shift = if self.top == 0 { 0 } else { counter - self.top };
|
||||
if shift > 0 {
|
||||
self.advance_window(shift);
|
||||
}
|
||||
self.top = counter;
|
||||
self.set_bit(counter);
|
||||
} else {
|
||||
self.set_bit(counter);
|
||||
}
|
||||
}
|
||||
|
||||
fn bit_index(&self, counter: u64) -> (usize, u64) {
|
||||
let offset = (counter % WINDOW_SIZE) as usize;
|
||||
let word = offset / 64;
|
||||
let bit = 1u64 << (offset % 64);
|
||||
(word, bit)
|
||||
}
|
||||
|
||||
fn is_set(&self, counter: u64) -> bool {
|
||||
let (word, bit) = self.bit_index(counter);
|
||||
self.bitmap[word] & bit != 0
|
||||
}
|
||||
|
||||
fn set_bit(&mut self, counter: u64) {
|
||||
let (word, bit) = self.bit_index(counter);
|
||||
self.bitmap[word] |= bit;
|
||||
}
|
||||
|
||||
fn advance_window(&mut self, shift: u64) {
|
||||
if shift >= WINDOW_SIZE {
|
||||
self.bitmap = [0u64; BITMAP_WORDS];
|
||||
return;
|
||||
}
|
||||
|
||||
for i in 1..=shift {
|
||||
let pos = self.top + i;
|
||||
let (word, bit) = self.bit_index(pos);
|
||||
self.bitmap[word] &= !bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplayGuard {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sequential_counters() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
for i in 1..=100 {
|
||||
assert!(guard.check(i).is_ok(), "counter {i} should pass");
|
||||
guard.accept(i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_zero() {
|
||||
let guard = ReplayGuard::new();
|
||||
assert!(guard.check(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_duplicate() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
assert!(guard.check(5).is_ok());
|
||||
guard.accept(5);
|
||||
assert!(guard.check(5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_order_within_window() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
for c in [10, 8, 9, 7] {
|
||||
assert!(guard.check(c).is_ok());
|
||||
guard.accept(c);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_too_old() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
guard.check(3000).unwrap();
|
||||
guard.accept(3000);
|
||||
assert!(guard.check(1).is_err());
|
||||
// 953 = 3000 - 2048 + 1 is the oldest allowed
|
||||
assert!(guard.check(953).is_ok());
|
||||
guard.accept(953);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_slides_correctly() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
for i in 1..=10 {
|
||||
guard.check(i).unwrap();
|
||||
guard.accept(i);
|
||||
}
|
||||
guard.check(2050).unwrap();
|
||||
guard.accept(2050);
|
||||
assert!(guard.check(1).is_err());
|
||||
assert!(guard.check(2049).is_ok());
|
||||
guard.accept(2049);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_jump() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
guard.check(1).unwrap();
|
||||
guard.accept(1);
|
||||
guard.check(1_000_000).unwrap();
|
||||
guard.accept(1_000_000);
|
||||
guard.check(999_999).unwrap();
|
||||
guard.accept(999_999);
|
||||
assert!(guard.check(1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_without_accept_does_not_burn() {
|
||||
let mut guard = ReplayGuard::new();
|
||||
// Check counter 5 but don't accept (simulates failed AEAD)
|
||||
assert!(guard.check(5).is_ok());
|
||||
// Counter 5 should still be valid since we never accepted it
|
||||
assert!(guard.check(5).is_ok());
|
||||
guard.accept(5);
|
||||
// Now it's burned
|
||||
assert!(guard.check(5).is_err());
|
||||
}
|
||||
}
|
||||
186
src/transport/session.rs
Normal file
186
src/transport/session.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, KeyInit, Payload},
|
||||
ChaCha20Poly1305, Nonce,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{Error, Result};
|
||||
use super::replay::ReplayGuard;
|
||||
|
||||
/// Minimum packet size: 8 (counter) + 16 (AEAD tag) for empty payload.
|
||||
const MIN_PACKET_LEN: usize = 8 + 16;
|
||||
|
||||
/// An established encrypted session providing confidentiality,
|
||||
/// integrity, and replay protection.
|
||||
pub struct Session {
|
||||
send_cipher: ChaCha20Poly1305,
|
||||
recv_cipher: ChaCha20Poly1305,
|
||||
send_counter: u64,
|
||||
recv_guard: ReplayGuard,
|
||||
transcript_hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub(crate) fn new(
|
||||
mut send_key: [u8; 32],
|
||||
mut recv_key: [u8; 32],
|
||||
transcript_hash: [u8; 32],
|
||||
) -> Self {
|
||||
let send_cipher = ChaCha20Poly1305::new((&send_key).into());
|
||||
let recv_cipher = ChaCha20Poly1305::new((&recv_key).into());
|
||||
|
||||
send_key.zeroize();
|
||||
recv_key.zeroize();
|
||||
|
||||
Session {
|
||||
send_cipher,
|
||||
recv_cipher,
|
||||
send_counter: 0,
|
||||
recv_guard: ReplayGuard::new(),
|
||||
transcript_hash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Transcript hash binding this session to a specific handshake.
|
||||
/// Sign this value for authentication proofs.
|
||||
pub fn transcript_hash(&self) -> &[u8; 32] {
|
||||
&self.transcript_hash
|
||||
}
|
||||
|
||||
/// Encrypt a plaintext message into a packet: `counter || ciphertext || tag`.
|
||||
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
self.send_counter = self
|
||||
.send_counter
|
||||
.checked_add(1)
|
||||
.ok_or(Error::SessionNotEstablished)?;
|
||||
|
||||
let counter_bytes = self.send_counter.to_le_bytes();
|
||||
let nonce = counter_to_nonce(self.send_counter);
|
||||
|
||||
let payload = Payload {
|
||||
msg: plaintext,
|
||||
aad: &counter_bytes,
|
||||
};
|
||||
|
||||
let ciphertext = self
|
||||
.send_cipher
|
||||
.encrypt(&nonce, payload)
|
||||
.map_err(|_| Error::DecryptionFailed)?;
|
||||
|
||||
let mut packet = Vec::with_capacity(8 + ciphertext.len());
|
||||
packet.extend_from_slice(&counter_bytes);
|
||||
packet.extend_from_slice(&ciphertext);
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
/// Decrypt a packet, checking for replay.
|
||||
/// Packet format: `counter (8 LE) || ciphertext || tag (16)`.
|
||||
pub fn decrypt(&mut self, packet: &[u8]) -> Result<Vec<u8>> {
|
||||
if packet.len() < MIN_PACKET_LEN {
|
||||
return Err(Error::InvalidPacket("packet too short"));
|
||||
}
|
||||
|
||||
let counter_bytes: [u8; 8] = packet[..8].try_into().unwrap();
|
||||
let counter = u64::from_le_bytes(counter_bytes);
|
||||
let ciphertext = &packet[8..];
|
||||
|
||||
// Phase 1: check without committing
|
||||
self.recv_guard.check(counter)?;
|
||||
|
||||
let nonce = counter_to_nonce(counter);
|
||||
let payload = Payload {
|
||||
msg: ciphertext,
|
||||
aad: &counter_bytes,
|
||||
};
|
||||
|
||||
let plaintext = self
|
||||
.recv_cipher
|
||||
.decrypt(&nonce, payload)
|
||||
.map_err(|_| Error::DecryptionFailed)?;
|
||||
|
||||
// Phase 2: AEAD succeeded, now commit the counter
|
||||
self.recv_guard.accept(counter);
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// Current send counter (for diagnostics / session management).
|
||||
pub fn send_counter(&self) -> u64 {
|
||||
self.send_counter
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand an 8-byte counter into a 12-byte ChaCha20Poly1305 nonce.
|
||||
/// First 4 bytes are zero; last 8 bytes are the LE counter.
|
||||
fn counter_to_nonce(counter: u64) -> Nonce {
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
nonce_bytes[4..12].copy_from_slice(&counter.to_le_bytes());
|
||||
Nonce::from(nonce_bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_session_pair() -> (Session, Session) {
|
||||
let send_key = [1u8; 32];
|
||||
let recv_key = [2u8; 32];
|
||||
let transcript = [0u8; 32];
|
||||
|
||||
let a = Session::new(send_key, recv_key, transcript);
|
||||
let b = Session::new(recv_key, send_key, transcript);
|
||||
(a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_roundtrip() {
|
||||
let (mut a, mut b) = test_session_pair();
|
||||
let packet = a.encrypt(b"hello").unwrap();
|
||||
let plain = b.decrypt(&packet).unwrap();
|
||||
assert_eq!(&plain, b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counter_increments() {
|
||||
let (mut a, mut b) = test_session_pair();
|
||||
for i in 1..=5u64 {
|
||||
let packet = a.encrypt(b"msg").unwrap();
|
||||
assert_eq!(a.send_counter(), i);
|
||||
b.decrypt(&packet).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_rejected() {
|
||||
let (mut a, mut b) = test_session_pair();
|
||||
let packet = a.encrypt(b"once").unwrap();
|
||||
b.decrypt(&packet).unwrap();
|
||||
assert!(b.decrypt(&packet).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tampered_packet_rejected() {
|
||||
let (mut a, mut b) = test_session_pair();
|
||||
let mut packet = a.encrypt(b"secret").unwrap();
|
||||
// Flip a byte in the ciphertext
|
||||
let last = packet.len() - 1;
|
||||
packet[last] ^= 0xFF;
|
||||
assert!(b.decrypt(&packet).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_direction_rejected() {
|
||||
let (mut a, _b) = test_session_pair();
|
||||
let packet = a.encrypt(b"data").unwrap();
|
||||
// a tries to decrypt its own packet — wrong cipher
|
||||
assert!(a.decrypt(&packet).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_payload() {
|
||||
let (mut a, mut b) = test_session_pair();
|
||||
let packet = a.encrypt(b"").unwrap();
|
||||
let plain = b.decrypt(&packet).unwrap();
|
||||
assert!(plain.is_empty());
|
||||
}
|
||||
}
|
||||
258
tests/test_zkac.py
Normal file
258
tests/test_zkac.py
Normal file
@ -0,0 +1,258 @@
|
||||
import zkac
|
||||
import pytest
|
||||
|
||||
|
||||
class TestKeypairAndPublicKey:
|
||||
def test_keypair_generates(self):
|
||||
kp = zkac.Keypair()
|
||||
pk = kp.public_key()
|
||||
assert isinstance(pk, zkac.PublicKey)
|
||||
|
||||
def test_pubkey_serialization(self):
|
||||
kp = zkac.Keypair()
|
||||
pk = kp.public_key()
|
||||
raw = pk.to_bytes()
|
||||
assert len(raw) == 32
|
||||
pk2 = zkac.PublicKey.from_bytes(raw)
|
||||
assert pk == pk2
|
||||
|
||||
def test_pubkey_repr(self):
|
||||
kp = zkac.Keypair()
|
||||
r = repr(kp.public_key())
|
||||
assert r.startswith("PublicKey(")
|
||||
assert len(r) == len("PublicKey()") + 64
|
||||
|
||||
def test_different_keypairs_different_pubkeys(self):
|
||||
pk1 = zkac.Keypair().public_key()
|
||||
pk2 = zkac.Keypair().public_key()
|
||||
assert pk1 != pk2
|
||||
|
||||
def test_invalid_pubkey_bytes(self):
|
||||
with pytest.raises(ValueError):
|
||||
zkac.PublicKey.from_bytes(b"\x00" * 31)
|
||||
with pytest.raises(ValueError):
|
||||
zkac.PublicKey.from_bytes(b"\xff" * 32)
|
||||
|
||||
def test_max_proof_constant(self):
|
||||
assert zkac.MAX_BBS_AUTH_PROOF_BYTES > 0
|
||||
|
||||
|
||||
class TestBbsCredentials:
|
||||
def test_role_id_deterministic(self):
|
||||
rid1 = zkac.role_id("admin")
|
||||
rid2 = zkac.role_id("admin")
|
||||
rid3 = zkac.role_id("reader")
|
||||
assert rid1 == rid2
|
||||
assert rid1 != rid3
|
||||
assert len(rid1) == 32
|
||||
|
||||
def test_full_blind_credential_flow(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("admin")
|
||||
|
||||
req = zkac.prepare_blind_request()
|
||||
blind_sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
blind_sig, req.member_secret(), req.prover_blind(), rid, 1, pk
|
||||
)
|
||||
|
||||
nonce = b"session-nonce"
|
||||
proof = cred.present(nonce)
|
||||
assert len(proof) > 0
|
||||
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
assert reg.verify_presentation(rid, proof, nonce)
|
||||
|
||||
def test_wrong_role_rejected(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("admin")
|
||||
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), rid, 1, pk
|
||||
)
|
||||
|
||||
nonce = b"nonce"
|
||||
proof = cred.present(nonce)
|
||||
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
wrong_rid = zkac.role_id("reader")
|
||||
reg.register_role(wrong_rid, issuer.public_key(), 1)
|
||||
assert not reg.verify_presentation(wrong_rid, proof, nonce)
|
||||
|
||||
def test_epoch_revocation(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("ops")
|
||||
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), rid, 1, pk
|
||||
)
|
||||
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
nonce = b"n1"
|
||||
assert reg.verify_presentation(rid, cred.present(nonce), nonce)
|
||||
|
||||
reg.set_epoch(rid, 2)
|
||||
nonce2 = b"n2"
|
||||
assert not reg.verify_presentation(rid, cred.present(nonce2), nonce2)
|
||||
|
||||
def test_issuer_key_serialization(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
pk_bytes = pk.to_bytes()
|
||||
pk2 = zkac.BbsPublicKey.from_bytes(pk_bytes)
|
||||
assert pk.to_bytes() == pk2.to_bytes()
|
||||
|
||||
sk_bytes = issuer.secret_key_bytes()
|
||||
issuer2 = zkac.BbsIssuer.from_secret_key(sk_bytes)
|
||||
assert issuer2.public_key().to_bytes() == pk_bytes
|
||||
|
||||
def test_presentations_unlinkable(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("admin")
|
||||
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), rid, 1, pk
|
||||
)
|
||||
|
||||
p1 = cred.present(b"nonce1")
|
||||
p2 = cred.present(b"nonce2")
|
||||
assert p1 != p2
|
||||
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
assert reg.verify_presentation(rid, p1, b"nonce1")
|
||||
assert reg.verify_presentation(rid, p2, b"nonce2")
|
||||
|
||||
def test_multiple_members(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("editors")
|
||||
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
nonce = b"shared-nonce"
|
||||
|
||||
for _ in range(3):
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), rid, 1, pk
|
||||
)
|
||||
proof = cred.present(nonce)
|
||||
assert reg.verify_presentation(rid, proof, nonce)
|
||||
|
||||
def test_credential_role_and_epoch(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("admin")
|
||||
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 42)
|
||||
cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), rid, 42, pk
|
||||
)
|
||||
assert cred.role_id() == rid
|
||||
assert cred.epoch() == 42
|
||||
|
||||
|
||||
class TestNodeHandshake:
|
||||
def _make_credential(self):
|
||||
issuer = zkac.BbsIssuer()
|
||||
pk = issuer.public_key()
|
||||
rid = zkac.role_id("admin")
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), rid, 1, pk
|
||||
)
|
||||
return issuer, pk, rid, cred
|
||||
|
||||
def test_full_handshake(self):
|
||||
_, pk, rid, cred = self._make_credential()
|
||||
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
|
||||
client = zkac.Node(zkac.Keypair())
|
||||
server = zkac.Node(zkac.Keypair())
|
||||
|
||||
pending, init_msg = client.connect()
|
||||
server_session, response_msg = server.accept(init_msg)
|
||||
client_session, auth_packet = client.complete_connect(
|
||||
pending, response_msg, cred
|
||||
)
|
||||
|
||||
verified_rid = server.verify_auth(server_session, auth_packet, reg)
|
||||
assert verified_rid == rid
|
||||
|
||||
pkt = client_session.encrypt(b"admin command")
|
||||
assert server_session.decrypt(pkt) == b"admin command"
|
||||
|
||||
pkt = server_session.encrypt(b"response")
|
||||
assert client_session.decrypt(pkt) == b"response"
|
||||
|
||||
def test_replay_rejected(self):
|
||||
_, pk, rid, cred = self._make_credential()
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
|
||||
client = zkac.Node(zkac.Keypair())
|
||||
server = zkac.Node(zkac.Keypair())
|
||||
|
||||
pending, init_msg = client.connect()
|
||||
server_session, response_msg = server.accept(init_msg)
|
||||
client_session, auth_packet = client.complete_connect(
|
||||
pending, response_msg, cred
|
||||
)
|
||||
server.verify_auth(server_session, auth_packet, reg)
|
||||
|
||||
pkt = client_session.encrypt(b"once")
|
||||
server_session.decrypt(pkt)
|
||||
with pytest.raises(ValueError, match="replay"):
|
||||
server_session.decrypt(pkt)
|
||||
|
||||
def test_tampered_packet_rejected(self):
|
||||
_, pk, rid, cred = self._make_credential()
|
||||
reg = zkac.RoleRegistry()
|
||||
reg.register_role(rid, pk, 1)
|
||||
|
||||
client = zkac.Node(zkac.Keypair())
|
||||
server = zkac.Node(zkac.Keypair())
|
||||
|
||||
pending, init_msg = client.connect()
|
||||
server_session, response_msg = server.accept(init_msg)
|
||||
client_session, auth_packet = client.complete_connect(
|
||||
pending, response_msg, cred
|
||||
)
|
||||
server.verify_auth(server_session, auth_packet, reg)
|
||||
|
||||
pkt = bytearray(client_session.encrypt(b"secret"))
|
||||
pkt[-1] ^= 0xFF
|
||||
with pytest.raises(ValueError, match="decryption"):
|
||||
server_session.decrypt(bytes(pkt))
|
||||
|
||||
def test_pending_connect_consumed(self):
|
||||
_, pk, rid, cred = self._make_credential()
|
||||
|
||||
client = zkac.Node(zkac.Keypair())
|
||||
server = zkac.Node(zkac.Keypair())
|
||||
|
||||
pending, init_msg = client.connect()
|
||||
_, response_msg = server.accept(init_msg)
|
||||
client.complete_connect(pending, response_msg, cred)
|
||||
|
||||
with pytest.raises(ValueError, match="consumed"):
|
||||
client.complete_connect(pending, response_msg, cred)
|
||||
855
uv.lock
generated
Normal file
855
uv.lock
generated
Normal file
@ -0,0 +1,855 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "appnope"
|
||||
version = "0.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asttokens"
|
||||
version = "3.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" },
|
||||
{ name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comm"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugpy"
|
||||
version = "1.8.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6b/668f21567e3250463beb6a401e7d598baa2a0907224000d7f68b9442c243/debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6", size = 2100484, upload-time = "2026-01-29T23:04:09.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/49/223143d1da586b891f35a45515f152742ad85bfc10d2e02e697f65c83b32/debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5", size = 3081272, upload-time = "2026-01-29T23:04:11.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/24/9f219c9290fe8bee4f63f9af8ebac440c802e6181d7f39a79abcb5fdff2f/debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee", size = 5285196, upload-time = "2026-01-29T23:04:13.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/f3/4a12d7b1b09e3b79ba6e3edfa0c677b8b8bdf110bc4b3607e0f29fb4e8b3/debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8", size = 5317163, upload-time = "2026-01-29T23:04:15.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "6.31.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" },
|
||||
{ name = "comm", marker = "python_full_version < '3.10'" },
|
||||
{ name = "debugpy", marker = "python_full_version < '3.10'" },
|
||||
{ name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version < '3.10'" },
|
||||
{ name = "nest-asyncio", marker = "python_full_version < '3.10'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.10'" },
|
||||
{ name = "psutil", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version < '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version < '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "7.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" },
|
||||
{ name = "comm", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "debugpy", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "nest-asyncio", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "psutil", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "8.18.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version < '3.10'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.10'" },
|
||||
{ name = "jedi", marker = "python_full_version < '3.10'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pygments", marker = "python_full_version < '3.10'" },
|
||||
{ name = "stack-data", marker = "python_full_version < '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "8.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "jedi", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "pygments", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "stack-data", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "traitlets", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "9.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "jedi", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "pexpect", marker = "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "pygments", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "stack-data", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "traitlets", marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/09/ba70f8d662d5671687da55ad2cc0064cf795b15e1eea70907532202e7c97/ipython-9.10.1-py3-none-any.whl", hash = "sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232", size = 622827, upload-time = "2026-03-27T09:53:24.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "9.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "jedi", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "pygments", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "stack-data", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython-pygments-lexers"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pygments", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jedi"
|
||||
version = "0.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "parso" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "8.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
|
||||
{ name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "python-dateutil", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version < '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version < '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "8.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "python-dateutil", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-core"
|
||||
version = "5.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-core"
|
||||
version = "5.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "platformdirs", version = "4.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
version = "0.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nest-asyncio"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parso"
|
||||
version = "0.8.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pexpect"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ptyprocess" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pure-eval"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyzmq"
|
||||
version = "27.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "implementation_name == 'pypy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stack-data"
|
||||
version = "0.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asttokens" },
|
||||
{ name = "executing" },
|
||||
{ name = "pure-eval" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zkac"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "ipykernel", version = "7.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "ipykernel", specifier = ">=6.31.0" }]
|
||||
Loading…
x
Reference in New Issue
Block a user