├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── publish.yml │ └── rust.yml ├── .gitignore ├── .readthedocs.yaml ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── bench ├── Makefile ├── bench_pyrtls.py └── bench_ssl.py ├── deny.toml ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ └── reference.rst ├── examples └── generate-certs.rs ├── pyproject.toml ├── pyrtls.pyi ├── src ├── client.rs ├── lib.rs └── server.rs ├── test.py └── tests ├── ca-certificate.pem ├── ee-certificate.pem └── ee-key.pem /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [djc] 2 | patreon: dochtman 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | crates-io: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: weekly 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | include: 16 | - os: ubuntu-latest 17 | target: x86_64 18 | - os: ubuntu-latest 19 | target: x86 20 | - os: windows-latest 21 | target: x64 22 | - os: windows-latest 23 | target: x86 24 | - os: macos-latest 25 | target: aarch64 26 | - os: macos-latest 27 | target: x64 28 | 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-python@v5 33 | - name: Build wheels 34 | uses: PyO3/maturin-action@v1 35 | with: 36 | target: ${{ matrix.target }} 37 | args: --release --locked --out dist --find-interpreter 38 | sccache: "true" 39 | manylinux: auto 40 | - run: ls -l dist/ 41 | - name: Upload wheels 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: wheels-${{ matrix.os }}-${{ matrix.target }} 45 | path: dist 46 | overwrite: true 47 | 48 | release: 49 | environment: "Publish wheels" 50 | name: Release 51 | runs-on: ubuntu-latest 52 | needs: [build] 53 | steps: 54 | - uses: actions/download-artifact@v4 55 | with: 56 | name: wheels-ubuntu-latest-x86_64 57 | - uses: actions/download-artifact@v4 58 | with: 59 | name: wheels-ubuntu-latest-x86 60 | - uses: actions/download-artifact@v4 61 | with: 62 | name: wheels-windows-latest-x64 63 | - uses: actions/download-artifact@v4 64 | with: 65 | name: wheels-windows-latest-x86 66 | - uses: actions/download-artifact@v4 67 | with: 68 | name: wheels-macos-latest-aarch64 69 | - uses: actions/download-artifact@v4 70 | with: 71 | name: wheels-macos-latest-x64 72 | - name: Publish to PyPI 73 | uses: PyO3/maturin-action@v1 74 | env: 75 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 76 | with: 77 | command: upload 78 | args: --non-interactive --skip-existing * 79 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | schedule: 8 | - cron: "25 6 * * 5" 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | rust: [stable, beta] 16 | exclude: 17 | - os: macos-latest 18 | rust: beta 19 | - os: windows-latest 20 | rust: beta 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@master 27 | with: 28 | toolchain: ${{ matrix.rust }} 29 | - run: cargo build --locked --all-features --all-targets 30 | 31 | msrv: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@master 36 | with: 37 | toolchain: 1.71.0 38 | - run: cargo check --lib --locked --all-features 39 | 40 | lint: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: dtolnay/rust-toolchain@stable 45 | with: 46 | components: rustfmt, clippy 47 | - run: cargo fmt --all -- --check 48 | - run: cargo clippy --locked --all-features --all-targets -- -D warnings 49 | 50 | audit: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: EmbarkStudios/cargo-deny-action@v2 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | *.so 4 | /docs/build 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 2 | 3 | version: 2 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.12" 8 | rust: latest 9 | python: 10 | install: 11 | - method: pip 12 | path: . 13 | sphinx: 14 | configuration: docs/source/conf.py 15 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.98" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.5.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 16 | 17 | [[package]] 18 | name = "base64" 19 | version = "0.22.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "2.9.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 28 | 29 | [[package]] 30 | name = "bytes" 31 | version = "1.10.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 34 | 35 | [[package]] 36 | name = "cc" 37 | version = "1.2.27" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" 40 | dependencies = [ 41 | "shlex", 42 | ] 43 | 44 | [[package]] 45 | name = "cesu8" 46 | version = "1.1.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 49 | 50 | [[package]] 51 | name = "cfg-if" 52 | version = "1.0.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 55 | 56 | [[package]] 57 | name = "combine" 58 | version = "4.6.7" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 61 | dependencies = [ 62 | "bytes", 63 | "memchr", 64 | ] 65 | 66 | [[package]] 67 | name = "core-foundation" 68 | version = "0.10.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 71 | dependencies = [ 72 | "core-foundation-sys", 73 | "libc", 74 | ] 75 | 76 | [[package]] 77 | name = "core-foundation-sys" 78 | version = "0.8.7" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 81 | 82 | [[package]] 83 | name = "deranged" 84 | version = "0.4.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 87 | dependencies = [ 88 | "powerfmt", 89 | ] 90 | 91 | [[package]] 92 | name = "getrandom" 93 | version = "0.2.16" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 96 | dependencies = [ 97 | "cfg-if", 98 | "libc", 99 | "wasi", 100 | ] 101 | 102 | [[package]] 103 | name = "heck" 104 | version = "0.5.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 107 | 108 | [[package]] 109 | name = "indoc" 110 | version = "2.0.6" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 113 | 114 | [[package]] 115 | name = "jni" 116 | version = "0.21.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 119 | dependencies = [ 120 | "cesu8", 121 | "cfg-if", 122 | "combine", 123 | "jni-sys", 124 | "log", 125 | "thiserror", 126 | "walkdir", 127 | "windows-sys 0.45.0", 128 | ] 129 | 130 | [[package]] 131 | name = "jni-sys" 132 | version = "0.3.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 135 | 136 | [[package]] 137 | name = "libc" 138 | version = "0.2.174" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 141 | 142 | [[package]] 143 | name = "log" 144 | version = "0.4.27" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 147 | 148 | [[package]] 149 | name = "memchr" 150 | version = "2.7.5" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 153 | 154 | [[package]] 155 | name = "memoffset" 156 | version = "0.9.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 159 | dependencies = [ 160 | "autocfg", 161 | ] 162 | 163 | [[package]] 164 | name = "num-conv" 165 | version = "0.1.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 168 | 169 | [[package]] 170 | name = "once_cell" 171 | version = "1.21.3" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 174 | 175 | [[package]] 176 | name = "openssl-probe" 177 | version = "0.1.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 180 | 181 | [[package]] 182 | name = "pem" 183 | version = "3.0.5" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" 186 | dependencies = [ 187 | "base64", 188 | "serde", 189 | ] 190 | 191 | [[package]] 192 | name = "portable-atomic" 193 | version = "1.11.1" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 196 | 197 | [[package]] 198 | name = "powerfmt" 199 | version = "0.2.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 202 | 203 | [[package]] 204 | name = "proc-macro2" 205 | version = "1.0.95" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 208 | dependencies = [ 209 | "unicode-ident", 210 | ] 211 | 212 | [[package]] 213 | name = "pyo3" 214 | version = "0.25.1" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" 217 | dependencies = [ 218 | "indoc", 219 | "libc", 220 | "memoffset", 221 | "once_cell", 222 | "portable-atomic", 223 | "pyo3-build-config", 224 | "pyo3-ffi", 225 | "pyo3-macros", 226 | "unindent", 227 | ] 228 | 229 | [[package]] 230 | name = "pyo3-build-config" 231 | version = "0.25.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" 234 | dependencies = [ 235 | "once_cell", 236 | "target-lexicon", 237 | ] 238 | 239 | [[package]] 240 | name = "pyo3-ffi" 241 | version = "0.25.1" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" 244 | dependencies = [ 245 | "libc", 246 | "pyo3-build-config", 247 | ] 248 | 249 | [[package]] 250 | name = "pyo3-macros" 251 | version = "0.25.1" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" 254 | dependencies = [ 255 | "proc-macro2", 256 | "pyo3-macros-backend", 257 | "quote", 258 | "syn", 259 | ] 260 | 261 | [[package]] 262 | name = "pyo3-macros-backend" 263 | version = "0.25.1" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" 266 | dependencies = [ 267 | "heck", 268 | "proc-macro2", 269 | "pyo3-build-config", 270 | "quote", 271 | "syn", 272 | ] 273 | 274 | [[package]] 275 | name = "pyrtls" 276 | version = "0.1.3" 277 | dependencies = [ 278 | "anyhow", 279 | "pyo3", 280 | "rcgen", 281 | "rustls", 282 | "rustls-platform-verifier", 283 | "socket2", 284 | "webpki-roots", 285 | ] 286 | 287 | [[package]] 288 | name = "quote" 289 | version = "1.0.40" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 292 | dependencies = [ 293 | "proc-macro2", 294 | ] 295 | 296 | [[package]] 297 | name = "rcgen" 298 | version = "0.14.1" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "218a7fbb357f6da42c9fd3610b1a5128d087d460e5386eaa5040705c464611dc" 301 | dependencies = [ 302 | "pem", 303 | "ring", 304 | "rustls-pki-types", 305 | "time", 306 | "yasna", 307 | ] 308 | 309 | [[package]] 310 | name = "ring" 311 | version = "0.17.14" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 314 | dependencies = [ 315 | "cc", 316 | "cfg-if", 317 | "getrandom", 318 | "libc", 319 | "untrusted", 320 | "windows-sys 0.52.0", 321 | ] 322 | 323 | [[package]] 324 | name = "rustls" 325 | version = "0.23.28" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" 328 | dependencies = [ 329 | "log", 330 | "once_cell", 331 | "ring", 332 | "rustls-pki-types", 333 | "rustls-webpki", 334 | "subtle", 335 | "zeroize", 336 | ] 337 | 338 | [[package]] 339 | name = "rustls-native-certs" 340 | version = "0.8.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 343 | dependencies = [ 344 | "openssl-probe", 345 | "rustls-pki-types", 346 | "schannel", 347 | "security-framework", 348 | ] 349 | 350 | [[package]] 351 | name = "rustls-pki-types" 352 | version = "1.12.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 355 | dependencies = [ 356 | "zeroize", 357 | ] 358 | 359 | [[package]] 360 | name = "rustls-platform-verifier" 361 | version = "0.6.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "eda84358ed17f1f354cf4b1909ad346e6c7bc2513e8c40eb08e0157aa13a9070" 364 | dependencies = [ 365 | "core-foundation", 366 | "core-foundation-sys", 367 | "jni", 368 | "log", 369 | "once_cell", 370 | "rustls", 371 | "rustls-native-certs", 372 | "rustls-platform-verifier-android", 373 | "rustls-webpki", 374 | "security-framework", 375 | "security-framework-sys", 376 | "webpki-root-certs", 377 | "windows-sys 0.59.0", 378 | ] 379 | 380 | [[package]] 381 | name = "rustls-platform-verifier-android" 382 | version = "0.1.1" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" 385 | 386 | [[package]] 387 | name = "rustls-webpki" 388 | version = "0.103.3" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 391 | dependencies = [ 392 | "ring", 393 | "rustls-pki-types", 394 | "untrusted", 395 | ] 396 | 397 | [[package]] 398 | name = "same-file" 399 | version = "1.0.6" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 402 | dependencies = [ 403 | "winapi-util", 404 | ] 405 | 406 | [[package]] 407 | name = "schannel" 408 | version = "0.1.27" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 411 | dependencies = [ 412 | "windows-sys 0.59.0", 413 | ] 414 | 415 | [[package]] 416 | name = "security-framework" 417 | version = "3.2.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 420 | dependencies = [ 421 | "bitflags", 422 | "core-foundation", 423 | "core-foundation-sys", 424 | "libc", 425 | "security-framework-sys", 426 | ] 427 | 428 | [[package]] 429 | name = "security-framework-sys" 430 | version = "2.14.0" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 433 | dependencies = [ 434 | "core-foundation-sys", 435 | "libc", 436 | ] 437 | 438 | [[package]] 439 | name = "serde" 440 | version = "1.0.219" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 443 | dependencies = [ 444 | "serde_derive", 445 | ] 446 | 447 | [[package]] 448 | name = "serde_derive" 449 | version = "1.0.219" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 452 | dependencies = [ 453 | "proc-macro2", 454 | "quote", 455 | "syn", 456 | ] 457 | 458 | [[package]] 459 | name = "shlex" 460 | version = "1.3.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 463 | 464 | [[package]] 465 | name = "socket2" 466 | version = "0.6.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 469 | dependencies = [ 470 | "libc", 471 | "windows-sys 0.59.0", 472 | ] 473 | 474 | [[package]] 475 | name = "subtle" 476 | version = "2.6.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 479 | 480 | [[package]] 481 | name = "syn" 482 | version = "2.0.104" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 485 | dependencies = [ 486 | "proc-macro2", 487 | "quote", 488 | "unicode-ident", 489 | ] 490 | 491 | [[package]] 492 | name = "target-lexicon" 493 | version = "0.13.2" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" 496 | 497 | [[package]] 498 | name = "thiserror" 499 | version = "1.0.69" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 502 | dependencies = [ 503 | "thiserror-impl", 504 | ] 505 | 506 | [[package]] 507 | name = "thiserror-impl" 508 | version = "1.0.69" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 511 | dependencies = [ 512 | "proc-macro2", 513 | "quote", 514 | "syn", 515 | ] 516 | 517 | [[package]] 518 | name = "time" 519 | version = "0.3.41" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 522 | dependencies = [ 523 | "deranged", 524 | "num-conv", 525 | "powerfmt", 526 | "serde", 527 | "time-core", 528 | ] 529 | 530 | [[package]] 531 | name = "time-core" 532 | version = "0.1.4" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 535 | 536 | [[package]] 537 | name = "unicode-ident" 538 | version = "1.0.18" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 541 | 542 | [[package]] 543 | name = "unindent" 544 | version = "0.2.4" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" 547 | 548 | [[package]] 549 | name = "untrusted" 550 | version = "0.9.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 553 | 554 | [[package]] 555 | name = "walkdir" 556 | version = "2.5.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 559 | dependencies = [ 560 | "same-file", 561 | "winapi-util", 562 | ] 563 | 564 | [[package]] 565 | name = "wasi" 566 | version = "0.11.1+wasi-snapshot-preview1" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 569 | 570 | [[package]] 571 | name = "webpki-root-certs" 572 | version = "1.0.1" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" 575 | dependencies = [ 576 | "rustls-pki-types", 577 | ] 578 | 579 | [[package]] 580 | name = "webpki-roots" 581 | version = "1.0.1" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" 584 | dependencies = [ 585 | "rustls-pki-types", 586 | ] 587 | 588 | [[package]] 589 | name = "winapi-util" 590 | version = "0.1.9" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 593 | dependencies = [ 594 | "windows-sys 0.59.0", 595 | ] 596 | 597 | [[package]] 598 | name = "windows-sys" 599 | version = "0.45.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 602 | dependencies = [ 603 | "windows-targets 0.42.2", 604 | ] 605 | 606 | [[package]] 607 | name = "windows-sys" 608 | version = "0.52.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 611 | dependencies = [ 612 | "windows-targets 0.52.6", 613 | ] 614 | 615 | [[package]] 616 | name = "windows-sys" 617 | version = "0.59.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 620 | dependencies = [ 621 | "windows-targets 0.52.6", 622 | ] 623 | 624 | [[package]] 625 | name = "windows-targets" 626 | version = "0.42.2" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 629 | dependencies = [ 630 | "windows_aarch64_gnullvm 0.42.2", 631 | "windows_aarch64_msvc 0.42.2", 632 | "windows_i686_gnu 0.42.2", 633 | "windows_i686_msvc 0.42.2", 634 | "windows_x86_64_gnu 0.42.2", 635 | "windows_x86_64_gnullvm 0.42.2", 636 | "windows_x86_64_msvc 0.42.2", 637 | ] 638 | 639 | [[package]] 640 | name = "windows-targets" 641 | version = "0.52.6" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 644 | dependencies = [ 645 | "windows_aarch64_gnullvm 0.52.6", 646 | "windows_aarch64_msvc 0.52.6", 647 | "windows_i686_gnu 0.52.6", 648 | "windows_i686_gnullvm", 649 | "windows_i686_msvc 0.52.6", 650 | "windows_x86_64_gnu 0.52.6", 651 | "windows_x86_64_gnullvm 0.52.6", 652 | "windows_x86_64_msvc 0.52.6", 653 | ] 654 | 655 | [[package]] 656 | name = "windows_aarch64_gnullvm" 657 | version = "0.42.2" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 660 | 661 | [[package]] 662 | name = "windows_aarch64_gnullvm" 663 | version = "0.52.6" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 666 | 667 | [[package]] 668 | name = "windows_aarch64_msvc" 669 | version = "0.42.2" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 672 | 673 | [[package]] 674 | name = "windows_aarch64_msvc" 675 | version = "0.52.6" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 678 | 679 | [[package]] 680 | name = "windows_i686_gnu" 681 | version = "0.42.2" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 684 | 685 | [[package]] 686 | name = "windows_i686_gnu" 687 | version = "0.52.6" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 690 | 691 | [[package]] 692 | name = "windows_i686_gnullvm" 693 | version = "0.52.6" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 696 | 697 | [[package]] 698 | name = "windows_i686_msvc" 699 | version = "0.42.2" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 702 | 703 | [[package]] 704 | name = "windows_i686_msvc" 705 | version = "0.52.6" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 708 | 709 | [[package]] 710 | name = "windows_x86_64_gnu" 711 | version = "0.42.2" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 714 | 715 | [[package]] 716 | name = "windows_x86_64_gnu" 717 | version = "0.52.6" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 720 | 721 | [[package]] 722 | name = "windows_x86_64_gnullvm" 723 | version = "0.42.2" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 726 | 727 | [[package]] 728 | name = "windows_x86_64_gnullvm" 729 | version = "0.52.6" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 732 | 733 | [[package]] 734 | name = "windows_x86_64_msvc" 735 | version = "0.42.2" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 738 | 739 | [[package]] 740 | name = "windows_x86_64_msvc" 741 | version = "0.52.6" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 744 | 745 | [[package]] 746 | name = "yasna" 747 | version = "0.5.2" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" 750 | dependencies = [ 751 | "time", 752 | ] 753 | 754 | [[package]] 755 | name = "zeroize" 756 | version = "1.8.1" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 759 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyrtls" 3 | version = "0.1.3" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | rust-version = "1.71" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | pyo3 = "0.25" 13 | rustls = { version = "0.23.15", default-features = false, features = ["logging", "ring", "std", "tls12"] } 14 | rustls-platform-verifier = "0.6" 15 | socket2 = "0.6" 16 | webpki-roots = "1" 17 | 18 | [dev-dependencies] 19 | anyhow = "1" 20 | rcgen = "0.14" 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test-python 2 | test-python: install 3 | python3 test.py 4 | 5 | .PHONY: install 6 | docs: install 7 | sphinx-build -M html docs/source docs/build 8 | 9 | .PHONY: install 10 | install: 11 | cargo build --release 12 | maturin build 13 | pip3 install --user --break-system-packages --force-reinstall target/wheels/*.whl 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyrtls: rustls-based modern TLS for Python 2 | 3 | [![Latest version](https://img.shields.io/pypi/v/pyrtls.svg)](https://pypi.org/project/pyrtls) 4 | [![Documentation](https://readthedocs.org/projects/pyrtls/badge/?version=latest)](https://pyrtls.readthedocs.io) 5 | [![CI](https://github.com/djc/pyrtls/workflows/CI/badge.svg?branch=main)](https://github.com/djc/pyrtls/actions?query=workflow%3ACI+branch%3Amain) 6 | 7 | pyrtls provides bindings to [rustls][rustls], a modern Rust-based TLS implementation with an API that is 8 | intended to be easy to use to replace the `ssl` module (but not entirely compatible with it). 9 | 10 | In addition to being memory-safe, the library is designed to be more secure by default. As such, 11 | it does not implement older protocol versions, cipher suites with known security problems, and 12 | some problematic features of the TLS protocol. For more details, review the [rustls manual][manual]. 13 | 14 | > [!WARNING] 15 | > This project is just getting started. While rustls is mature, the Python bindings 16 | > are pretty new and not yet feature-complete. Please consider helping out (see below). 17 | 18 | [rustls]: https://github.com/rustls/rustls 19 | [manual]: https://docs.rs/rustls/latest/rustls/manual/index.html 20 | 21 | ## Why? 22 | 23 | To bring the security and performance of rustls to the Python world. 24 | 25 | So far this is a side project. Please consider helping out: 26 | 27 | * Please help fund this work on [GitHub Sponsors](https://github.com/sponsors/djc) 28 | * Pull requests welcome, of course! 29 | * Feedback through [issues] is highly appreciated 30 | * If you're interested in commercial support, please contact me 31 | 32 | [issues]: https://github.com/djc/pyrtls/issues 33 | 34 | ## Features 35 | 36 | - Support for TLS 1.2 and 1.3 37 | - Support for commonly used secure cipher suites 38 | - Support for ALPN protocol negotiation 39 | - Support for Server Name Indication (SNI) 40 | - Support for session resumption 41 | - Clients use the OS certificate trust store by default 42 | - Exposes socket wrapper as well as [sans I/O][sans-io] APIs 43 | - In basic tests, performance is comparable to the `ssl` module 44 | 45 | [sans-io]: https://sans-io.readthedocs.io/ 46 | 47 | Not implemented 48 | --------------- 49 | 50 | - TLS 1.1 and older versions of the protocol 51 | - Older cipher suites with security problems 52 | - Using CA certificates directly to authenticate a server/client (often called self-signed 53 | certificates). The built-in certificate verifier does not support using a trust anchor 54 | as both a CA certificate and an end-entity certificate, in order to limit complexity and 55 | risk in path building. 56 | -------------------------------------------------------------------------------- /bench/Makefile: -------------------------------------------------------------------------------- 1 | ssl: 2 | python3 -m timeit -r 20 -s "import bench_ssl; ctx = bench_ssl.setup()" "bench_ssl.request(ctx)" 3 | 4 | pyrtls: 5 | python3 -m timeit -r 20 -s "import bench_pyrtls; config = bench_pyrtls.setup()" "bench_pyrtls.request(config)" 6 | -------------------------------------------------------------------------------- /bench/bench_pyrtls.py: -------------------------------------------------------------------------------- 1 | import pyrtls 2 | import socket 3 | import sys 4 | 5 | def setup(): 6 | return pyrtls.ClientConfig() 7 | 8 | def request(config): 9 | sock = socket.socket() 10 | sock = config.wrap_socket(sock, 'xavamedia.nl') 11 | sock.connect(('xavamedia.nl', 443)) 12 | sock.send(b'GET / HTTP/1.1\r\nHost: xavamedia.nl\r\nConnection: close\r\n\r\n') 13 | return sock.recv(8192) 14 | 15 | if __name__ == '__main__': 16 | print(request(setup())) 17 | -------------------------------------------------------------------------------- /bench/bench_ssl.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import socket 3 | import sys 4 | 5 | def setup(): 6 | return ssl.create_default_context() 7 | 8 | def request(ctx): 9 | sock = socket.socket() 10 | sock = ctx.wrap_socket(sock, server_hostname='xavamedia.nl') 11 | sock.connect(('xavamedia.nl', 443)) 12 | sock.send(b'GET / HTTP/1.1\r\nHost: xavamedia.nl\r\nConnection: close\r\n\r\n') 13 | return sock.recv(8192) 14 | 15 | if __name__ == '__main__': 16 | print(request(setup())) 17 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | version = 2 3 | allow = [ 4 | "Apache-2.0", 5 | "Apache-2.0 WITH LLVM-exception", 6 | "BSD-3-Clause", 7 | "CDLA-Permissive-2.0", 8 | "ISC", 9 | "MIT", 10 | "Unicode-3.0", 11 | ] 12 | 13 | [[licenses.clarify]] 14 | name = "ring" 15 | version = "*" 16 | expression = "MIT AND ISC AND OpenSSL" 17 | license-files = [ 18 | { path = "LICENSE", hash = 0xbd0eed23 } 19 | ] 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'pyrtls' 10 | copyright = '2023, Dirkjan Ochtman' 11 | author = 'Dirkjan Ochtman' 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = [ 17 | 'sphinx.ext.autodoc', 18 | ] 19 | 20 | templates_path = ['_templates'] 21 | exclude_patterns = [] 22 | 23 | 24 | 25 | # -- Options for HTML output ------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 27 | 28 | html_theme = 'alabaster' 29 | html_static_path = ['_static'] 30 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | pyrtls 2 | ====== 3 | 4 | pyrtls provides bindings to `rustls`_, a modern Rust-based TLS implementation with an API that is 5 | intended to be easy to use to replace the `ssl` module (but not entirely compatible with it). 6 | 7 | In addition to being memory-safe, the library is designed to be more secure by default. As such, 8 | it does not implement older protocol versions, cipher suites with known security problems, and 9 | some problematic features of the TLS protocol. For more details, review the `rustls manual`_. 10 | 11 | .. _rustls: https://github.com/rustls/rustls 12 | .. _rustls manual: https://docs.rs/rustls/latest/rustls/manual/index.html 13 | 14 | Features 15 | -------- 16 | 17 | - Support for TLS 1.2 and 1.3 18 | - Support for commonly used secure cipher suites 19 | - Support for ALPN protocol negotiation 20 | - Support for Server Name Indication (SNI) 21 | - Support for session resumption 22 | - Clients use the OS certificate trust store by default 23 | - Exposes socket wrapper as well as `sans I/O`_ APIs 24 | - In basic tests, performance is comparable to the `ssl` module 25 | 26 | .. _sans I/O: https://sans-io.readthedocs.io/ 27 | 28 | Not implemented 29 | --------------- 30 | 31 | - TLS 1.1 and older versions of the protocol 32 | - Older cipher suites with security problems 33 | - Using CA certificates directly to authenticate a server/client (often called self-signed 34 | certificates). The built-in certificate verifier does not support using a trust anchor 35 | as both a CA certificate and an end-entity certificate, in order to limit complexity and 36 | risk in path building. 37 | 38 | API reference 39 | ============= 40 | 41 | .. toctree:: 42 | :maxdepth: 2 43 | 44 | reference 45 | 46 | Indices and tables 47 | ================== 48 | 49 | * :ref:`genindex` 50 | * :ref:`modindex` 51 | * :ref:`search` 52 | -------------------------------------------------------------------------------- /docs/source/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. module:: pyrtls 5 | 6 | .. autoclass:: ClientSocket 7 | :members: 8 | 9 | .. autoclass:: ClientConnection 10 | :members: 11 | 12 | .. autoclass:: ClientConfig 13 | :members: 14 | 15 | .. autoclass:: ServerSocket 16 | :members: 17 | 18 | .. autoclass:: ServerConnection 19 | :members: 20 | 21 | .. autoclass:: ServerConfig 22 | :members: 23 | 24 | .. autoclass:: IoState 25 | :members: 26 | 27 | .. autoclass:: TrustAnchor 28 | :members: 29 | 30 | .. autoclass:: TLSError 31 | :members: 32 | -------------------------------------------------------------------------------- /examples/generate-certs.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use rcgen::{BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair}; 4 | 5 | fn main() -> Result<(), anyhow::Error> { 6 | let mut ca_params = CertificateParams::new(vec!["localhost".to_owned()])?; 7 | ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 8 | let ca_key = KeyPair::generate()?; 9 | let ca_cert = ca_params.self_signed(&ca_key)?; 10 | fs::write("tests/ca-certificate.pem", ca_cert.pem())?; 11 | let ca = Issuer::new(ca_params, ca_key); 12 | 13 | let ee_params = CertificateParams::new(vec!["localhost".to_owned()])?; 14 | let ee_key = KeyPair::generate()?; 15 | let ee_cert = ee_params.signed_by(&ee_key, &ca)?; 16 | fs::write("tests/ee-certificate.pem", ee_cert.pem())?; 17 | fs::write("tests/ee-key.pem", ee_key.serialize_pem())?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1,<2"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "pyrtls" 7 | requires-python = ">=3.7" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | 14 | [tool.maturin] 15 | features = ["pyo3/extension-module"] 16 | -------------------------------------------------------------------------------- /pyrtls.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | # pyrtls: rustls-based modern TLS for Python 3 | 4 | [Latest version](https://pypi.org/project/pyrtls) | 5 | [Documentation](https://pyrtls.readthedocs.io) | 6 | [CI](https://github.com/djc/pyrtls/actions?query=workflow%3ACI+branch%3Amain) 7 | 8 | pyrtls provides bindings to [rustls](https://github.com/rustls/rustls), a modern Rust-based TLS implementation with an API that is 9 | intended to be easy to use to replace the `ssl` module (but not entirely compatible with it). 10 | 11 | In addition to being memory-safe, the library is designed to be more secure by default. As such, 12 | it does not implement older protocol versions, cipher suites with known security problems, and 13 | some problematic features of the TLS protocol. For more details, review the [manual](https://docs.rs/rustls/latest/rustls/manual/index.html). 14 | 15 | ## Warning! 16 | 17 | This project is just getting started. While rustls is mature, the Python bindings 18 | are pretty new and not yet feature-complete. 19 | """ 20 | 21 | from collections.abc import Iterable 22 | from socket import socket 23 | 24 | # lib.rs 25 | 26 | class TrustAnchor: 27 | """ 28 | A `TrustAnchor` represents a trusted authority for verifying certificates. These are used 29 | to verify the chain of trust from a certificate to a trusted root for server certificates. 30 | All arguments must be `bytes` instances containing DER-encoded data. 31 | """ 32 | def __new__( 33 | cls, 34 | subject: bytes, 35 | subject_public_key_info: bytes, 36 | name_constraints: bytes | None, 37 | ) -> TrustAnchor: ... 38 | 39 | class IoState: 40 | def tls_bytes_to_write(self) -> int: 41 | """ 42 | How many bytes could be written by `Connection.write_tls_into()` if called right now. 43 | A non-zero value implies that `Connection.wants_write()` would yield `True`. 44 | """ 45 | 46 | def plaintext_bytes_to_read(self) -> int: 47 | """How many plaintext bytes are currently buffered in the connection.""" 48 | 49 | def peer_has_closed(self) -> bool: 50 | """ 51 | `True` if the peer has sent us a `close_notify` alert. This is the TLS mechanism to 52 | securely half-close a TLS connection, and signifies that the peer will not send any 53 | further data on this connection. 54 | """ 55 | 56 | class TLSError: 57 | """ 58 | `TLSError` represents any errors coming from pyrtls. Its string representation 59 | contains more detailed error information. 60 | """ 61 | 62 | # client.rs 63 | 64 | class ClientSocket: 65 | """ 66 | A `ClientSocket` is a wrapper type that contains both a `socket.socket` and a 67 | `ClientConnection` object. It is similar to the `ssl.SSLSocket` class from the 68 | standard library and should implement most of the same methods. 69 | """ 70 | 71 | def connect(self, address: tuple[str, int]) -> None: 72 | """ 73 | Connect to a remote socket address. `address` must currently be a 2-element 74 | tuple containing a hostname and a port number. 75 | """ 76 | 77 | def do_handshake(self) -> None: 78 | """Perform the TLS setup handshake.""" 79 | 80 | def send(self, bytes: bytes) -> int: 81 | """ 82 | Send data to the socket. The socket must be connected to a remote socket. Returns the 83 | number of bytes sent. Applications are responsible for checking that all data has been 84 | sent; if only some of the data was transmitted, the application needs to attempt delivery 85 | of the remaining data. 86 | """ 87 | 88 | def recv(self, size: int) -> bytes: 89 | """ 90 | Receive data from the socket. The return value is a bytes object representing the data 91 | received. The maximum amount of data to be received at once is specified by `size`. 92 | A returned empty bytes object indicates that the server has disconnected. 93 | """ 94 | 95 | class ClientConnection: 96 | """ 97 | A `ClientConnection` contains TLS state associated with a single client-side connection. 98 | It does not contain any networking state, and is not directly associated with a socket, 99 | so I/O happens via the methods on this object directly. 100 | 101 | A `ClientConnection` can be created from a `ClientConfig` `config` and a server name, `name`. 102 | The server name must be either a DNS hostname or an IP address (only string forms are 103 | currently accepted). 104 | """ 105 | 106 | def __new__(cls, config: ClientConfig, name: str) -> ClientConnection: ... 107 | def readable(self) -> bool: 108 | """ 109 | Returns `true` if the caller should call `read_tls()` as soon as possible. 110 | 111 | If there is pending plaintext data to read, this returns `false`. If your application 112 | respects this mechanism, only one full TLS message will be buffered by pyrtls. 113 | """ 114 | 115 | def read_tls(self, buf: bytes) -> int: 116 | """ 117 | Read TLS content from `buf` into the internal buffer. Return the number of bytes read, or 118 | `0` once a `close_notify` alert has been received. No additional data is read in this 119 | state. 120 | 121 | Due to internal buffering, `buf` may contain TLS messages in arbitrary-sized chunks (like 122 | a socket or pipe might). 123 | 124 | You should call `process_new_packets()` each time a call to this function succeeds in 125 | order to empty the incoming TLS data buffer. 126 | 127 | Exceptions may be raised to signal backpressure: 128 | 129 | * In order to empty the incoming TLS data buffer, you should call `process_new_packets()` 130 | each time a call to this function succeeds. 131 | * In order to empty the incoming plaintext data buffer, you should call `read_into()` 132 | after the call to `process_new_packets()`. 133 | 134 | You should call `process_new_packets()` each time a call to this function succeeds in 135 | order to empty the incoming TLS data buffer 136 | 137 | Mirrors the `RawIO.write()` interface. 138 | """ 139 | 140 | def process_new_packets(self) -> IoState: 141 | """ 142 | Processes any new packets read by a previous call to `read_tls()`. 143 | 144 | Errors from this function relate to TLS protocol errors, and are fatal to the connection. 145 | Future calls after an error will do no new work and will return the same error. After an 146 | error is returned from `process_new_packets()`, you should not call `read_tls()` anymore 147 | (it will fill up buffers to no purpose). However, you may call the other methods on the 148 | connection, including `write()` and `write_tls_into()`. Most likely you will want to 149 | call `write_tls_into()` to send any alerts queued by the error and then close the 150 | underlying connection. 151 | 152 | In case of success, yields an `IoState` object with sundry state about the connection. 153 | """ 154 | 155 | def write(self, buf: bytes) -> int: 156 | """ 157 | Send the plaintext `buf` to the peer, encrypting and authenticating it. Once this 158 | function succeeds you should call `write_tls_into()` which will output the 159 | corresponding TLS records. 160 | 161 | This function buffers plaintext sent before the TLS handshake completes, and sends it as 162 | soon as it can. 163 | """ 164 | 165 | def writable(self) -> bool: 166 | """Returns `True` if the caller should call `write_tls_into()` as soon as possible.""" 167 | 168 | def write_tls_into(self, buf: bytearray) -> int: 169 | """ 170 | Writes TLS messages into `buf`. 171 | 172 | On success, this function returns the number of bytes written (after encoding and 173 | encryption). 174 | 175 | After this function returns, the connection buffer may not yet be fully flushed. 176 | `writable()` can be used to check if the output buffer is empty. 177 | 178 | Mirrors the `RawIO.readinto()` interface. 179 | """ 180 | 181 | def read_into(self, buf: bytearray) -> int: 182 | """ 183 | Obtain plaintext data received from the peer over this TLS connection. 184 | 185 | If the peer closes the TLS connection cleanly, this returns `0` once all the pending data 186 | has been read. No further data can be received on that connection, so the underlying TCP 187 | connection should be half-closed too. 188 | 189 | If the peer closes the TLS connection uncleanly (a TCP EOF without sending a 190 | `close_notify` alert) this functions raises a `TlsError` exception once any pending data 191 | has been read. 192 | 193 | Note that support for `close_notify` varies in peer TLS libraries: many do not support it 194 | and uncleanly close the TCP connection (this might be vulnerable to truncation attacks 195 | depending on the application protocol). This means applications using pyrtls must both 196 | handle EOF from this function, **and** unexpected EOF of the underlying TCP connection. 197 | 198 | If there are no bytes to read, this raises a `TlsError` exception. 199 | 200 | You may learn the number of bytes available at any time by inspecting the return of 201 | `process_new_packets()`. 202 | """ 203 | 204 | class ClientConfig: 205 | """ 206 | Create a new `ClientConfig` object (similar to `ssl.SSLContext`). A new `ClientConnection` can 207 | only be created by passing in a reference to a `ClientConfig` object. 208 | 209 | The most important configuration for `ClientConfig` is the certificate verification process. 210 | Three different options are offered to define the desired process: 211 | 212 | - `platform_verifier` (enabled by default) will enable the platform's certificate verifier 213 | on platforms that have on, and searching for CA certificates in the system trust store on 214 | other platforms (like Linux and FreeBSD). 215 | - `mozilla_roots` will enable a built-in set of Mozilla root certificates. This is independent 216 | of the operating system, but depends on the pyrtls package to deliver timely updates. 217 | - `custom_roots` allows the caller to specify an iterable of trust anchors. Each item must be: 218 | - A `TrustAnchor` object, which is a wrapper around a `webpki::TrustAnchor` object 219 | - A `bytes` object containing a DER-encoded certificate 220 | - A `str` object containing one PEM-encoded certificate 221 | 222 | The `platform_verifier` option cannot currently be combined with `mozilla_roots` or 223 | `custom_roots` (this will raise a `ValueError`), but the latter two can be combined. 224 | 225 | Other options: 226 | 227 | - `alpn_protocols` must be an iterable containing `bytes` or `str` objects, each representing 228 | one ALPN protocol string. 229 | """ 230 | 231 | def __new__( 232 | cls, 233 | *, 234 | platform_verifier: bool = True, 235 | mozilla_roots: bool = False, 236 | custom_roots: Iterable[TrustAnchor | bytes | str] | None = None, 237 | alpn_protocols: Iterable[bytes | str] | None = None, 238 | ) -> ClientConfig: ... 239 | def wrap_socket( 240 | self, sock: socket, server_hostname: str, do_handshake_on_connect: bool = True 241 | ) -> ClientSocket: 242 | """ 243 | Use the `ClientConfig` and the given `sock` to create a `ClientSocket`. 244 | 245 | Returns a `ClientSocket` if successful. Raises a `ValueError` if `server_hostname` 246 | is not a valid server name (either a DNS name or an IP address). 247 | """ 248 | 249 | # server.rs 250 | 251 | class ServerSocket: 252 | def bind(self, address: tuple[str, int]) -> None: 253 | """ 254 | Bind to the given `address`. `address` must currently be a 2-element tuple 255 | containing a hostname and a port number. 256 | """ 257 | 258 | def do_handshake(self) -> None: 259 | """Perform the TLS setup handshake.""" 260 | 261 | def send(self, bytes: bytes) -> int: 262 | """ 263 | Send data to the socket. The socket must be connected to a remote socket. Returns the 264 | number of bytes sent. Applications are responsible for checking that all data has been 265 | sent; if only some of the data was transmitted, the application needs to attempt delivery 266 | of the remaining data. 267 | """ 268 | 269 | def recv(self, size: int) -> bytes: 270 | """ 271 | Receive data from the socket. The return value is a bytes object representing the data 272 | received. The maximum amount of data to be received at once is specified by `size`. 273 | A returned empty bytes object indicates that the client has disconnected. 274 | """ 275 | 276 | class ServerConnection: 277 | """ 278 | A `ServerConnection` contains TLS state associated with a single server-side connection. 279 | It does not contain any networking state, and is not directly associated with a socket, 280 | so I/O happens via the methods on this object directly. 281 | 282 | A `ServerConnection` can be created from a `ServerConfig` `config`. 283 | """ 284 | 285 | def __new__(cls, config: ServerConfig) -> ServerConnection: ... 286 | def readable(self) -> bool: 287 | """ 288 | Returns `true` if the caller should call `read_tls()` as soon as possible. 289 | 290 | If there is pending plaintext data to read, this returns `false`. If your application 291 | respects this mechanism, only one full TLS message will be buffered by pyrtls. 292 | """ 293 | 294 | def read_tls(self, buf: bytes) -> int: 295 | """ 296 | Read TLS content from `buf` into the internal buffer. Return the number of bytes read, or 297 | `0` once a `close_notify` alert has been received. No additional data is read in this 298 | state. 299 | 300 | Due to internal buffering, `buf` may contain TLS messages in arbitrary-sized chunks (like 301 | a socket or pipe might). 302 | 303 | You should call `process_new_packets()` each time a call to this function succeeds in 304 | order to empty the incoming TLS data buffer. 305 | 306 | Exceptions may be raised to signal backpressure: 307 | 308 | * In order to empty the incoming TLS data buffer, you should call `process_new_packets()` 309 | each time a call to this function succeeds. 310 | * In order to empty the incoming plaintext data buffer, you should call `read_into()` 311 | after the call to `process_new_packets()`. 312 | 313 | You should call `process_new_packets()` each time a call to this function succeeds in 314 | order to empty the incoming TLS data buffer 315 | 316 | Mirrors the `RawIO.write()` interface. 317 | """ 318 | 319 | def process_new_packets(self) -> IoState: 320 | """ 321 | Processes any new packets read by a previous call to `read_tls()`. 322 | 323 | Errors from this function relate to TLS protocol errors, and are fatal to the connection. 324 | Future calls after an error will do no new work and will return the same error. After an 325 | error is returned from `process_new_packets()`, you should not call `read_tls()` anymore 326 | (it will fill up buffers to no purpose). However, you may call the other methods on the 327 | connection, including `write()` and `write_tls_into()`. Most likely you will want to 328 | call `write_tls_into()` to send any alerts queued by the error and then close the 329 | underlying connection. 330 | 331 | In case of success, yields an `IoState` object with sundry state about the connection. 332 | """ 333 | 334 | def write(self, buf: bytes) -> int: 335 | """ 336 | Send the plaintext `buf` to the peer, encrypting and authenticating it. Once this 337 | function succeeds you should call `write_tls_into()` which will output the 338 | corresponding TLS records. 339 | 340 | This function buffers plaintext sent before the TLS handshake completes, and sends it as 341 | soon as it can. 342 | """ 343 | 344 | def writable(self) -> bool: 345 | """Returns `True` if the caller should call `write_tls_into()` as soon as possible.""" 346 | 347 | def write_tls_into(self, buf: bytearray) -> int: 348 | """ 349 | Writes TLS messages into `buf`. 350 | 351 | On success, this function returns the number of bytes written (after encoding and 352 | encryption). 353 | 354 | After this function returns, the connection buffer may not yet be fully flushed. 355 | `writable()` can be used to check if the output buffer is empty. 356 | 357 | Mirrors the `RawIO.readinto()` interface. 358 | """ 359 | 360 | def read_into(self, buf: bytearray) -> int: 361 | """ 362 | Obtain plaintext data received from the peer over this TLS connection. 363 | 364 | If the peer closes the TLS connection cleanly, this returns `0` once all the pending data 365 | has been read. No further data can be received on that connection, so the underlying TCP 366 | connection should be half-closed too. 367 | 368 | If the peer closes the TLS connection uncleanly (a TCP EOF without sending a 369 | `close_notify` alert) this functions raises a `TlsError` exception once any pending data 370 | has been read. 371 | 372 | Note that support for `close_notify` varies in peer TLS libraries: many do not support it 373 | and uncleanly close the TCP connection (this might be vulnerable to truncation attacks 374 | depending on the application protocol). This means applications using pyrtls must both 375 | handle EOF from this function, **and** unexpected EOF of the underlying TCP connection. 376 | 377 | If there are no bytes to read, this raises a `TlsError` exception. 378 | 379 | You may learn the number of bytes available at any time by inspecting the return of 380 | `process_new_packets()`. 381 | """ 382 | 383 | class ServerConfig: 384 | """ 385 | Create a new `ServerConfig` object (similar to `ssl.SSLContext`). A new `ServerConnection` can 386 | only be created by passing in a reference to a `ServerConfig` object. 387 | 388 | The important configuration for `ServerConfig` is the certificate to supply to connecting 389 | clients, and the private key used to prove ownership of the certificate. 390 | 391 | Positional (mandatory) arguments: 392 | 393 | - `cert_chain`: an iterable, where each value must be of type `bytes` (representing the 394 | certificate encoded in DER) or `str` (with the certificate encoded in PEM). 395 | - `private_key`: a `bytes` or `str` object, containing the private key encoded in DER or PEM 396 | respectively. The private key can be in PKCS#1, PKCS#8, or SEC1 format. 397 | 398 | Other options: 399 | 400 | - `alpn_protocols` must be an iterable containing `bytes` or `str` objects, each representing 401 | one ALPN protocol string. 402 | """ 403 | 404 | def __new__( 405 | cls, 406 | cert_chain: Iterable[bytes | str], 407 | private_key: bytes | str, 408 | *, 409 | alpn_protocols: Iterable[bytes | str] | None = None, 410 | ) -> ServerConfig: ... 411 | def wrap_socket(self, sock: socket) -> ServerSocket: 412 | """ 413 | Use the `ServerConfig` and the given `sock` to create a `ServerSocket`. 414 | 415 | Returns a `ServerSocket` if successful. 416 | """ 417 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read, Write}; 2 | use std::net::ToSocketAddrs; 3 | use std::sync::Arc; 4 | 5 | use pyo3::exceptions::{PyException, PyValueError}; 6 | use pyo3::types::{ 7 | PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes, PyString, PyStringMethods, PyTuple, 8 | PyTupleMethods, 9 | }; 10 | use pyo3::{pyclass, pymethods, Bound, PyAny, PyResult, Python}; 11 | use rustls::pki_types::{CertificateDer, ServerName}; 12 | use rustls::RootCertStore; 13 | use rustls_platform_verifier::BuilderVerifierExt; 14 | 15 | use super::{IoState, SessionState, TlsError}; 16 | use crate::{extract_alpn_protocols, py_to_cert_der, py_to_pem, TrustAnchor}; 17 | 18 | /// A `ClientSocket` is a wrapper type that contains both a `socket.socket` and a 19 | /// `ClientConnection` object. It is similar to the `ssl.SSLSocket` class from the 20 | /// standard library and should implement most of the same methods. 21 | #[pyclass] 22 | pub(crate) struct ClientSocket { 23 | state: SessionState, 24 | do_handshake_on_connect: bool, 25 | } 26 | 27 | #[pymethods] 28 | impl ClientSocket { 29 | /// Connect to a remote socket address. `address` must currently be a 2-element 30 | /// tuple containing a hostname and a port number. 31 | fn connect(&mut self, address: &Bound<'_, PyTuple>) -> PyResult<()> { 32 | if address.len() != 2 { 33 | return Err(PyValueError::new_err( 34 | "only 2-element address tuples are supported", 35 | )); 36 | } 37 | 38 | let host = address.get_item(0)?; 39 | let host = host.extract::<&str>()?; 40 | let port = address.get_item(1)?.extract::()?; 41 | let addr = match (host, port).to_socket_addrs()?.next() { 42 | Some(addr) => addr, 43 | None => { 44 | return Err(PyValueError::new_err( 45 | "unable to convert address to socket address", 46 | )) 47 | } 48 | }; 49 | 50 | self.state.socket.connect(&addr.into())?; 51 | if self.do_handshake_on_connect { 52 | self.state.do_handshake()?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | /// Perform the TLS setup handshake. 59 | fn do_handshake(&mut self) -> PyResult<()> { 60 | self.state.do_handshake() 61 | } 62 | 63 | /// Send data to the socket. The socket must be connected to a remote socket. Returns the 64 | /// number of bytes sent. Applications are responsible for checking that all data has been 65 | /// sent; if only some of the data was transmitted, the application needs to attempt delivery 66 | /// of the remaining data. 67 | fn send(&mut self, bytes: &Bound<'_, PyBytes>) -> PyResult { 68 | self.state.send(bytes) 69 | } 70 | 71 | /// Receive data from the socket. The return value is a bytes object representing the data 72 | /// received. The maximum amount of data to be received at once is specified by `size`. 73 | /// A returned empty bytes object indicates that the server has disconnected. 74 | fn recv<'p>(&mut self, size: usize, py: Python<'p>) -> PyResult> { 75 | self.state.recv(size, py) 76 | } 77 | } 78 | 79 | /// A `ClientConnection` contains TLS state associated with a single client-side connection. 80 | /// It does not contain any networking state, and is not directly associated with a socket, 81 | /// so I/O happens via the methods on this object directly. 82 | /// 83 | /// A `ClientConnection` can be created from a `ClientConfig` `config` and a server name, `name`. 84 | /// The server name must be either a DNS hostname or an IP address (only string forms are 85 | /// currently accepted). 86 | #[pyclass] 87 | pub(crate) struct ClientConnection { 88 | inner: rustls::ClientConnection, 89 | } 90 | 91 | #[pymethods] 92 | impl ClientConnection { 93 | #[new] 94 | fn new(config: &ClientConfig, name: &Bound<'_, PyString>) -> PyResult { 95 | let name = match ServerName::try_from(name.to_str()?) { 96 | Ok(n) => n.to_owned(), 97 | Err(_) => return Err(PyValueError::new_err("invalid hostname")), 98 | }; 99 | 100 | Ok(Self { 101 | inner: rustls::ClientConnection::new(config.inner.clone(), name) 102 | .map_err(TlsError::from)?, 103 | }) 104 | } 105 | 106 | /// Returns `true` if the caller should call `read_tls()` as soon as possible. 107 | /// 108 | /// If there is pending plaintext data to read, this returns `false`. If your application 109 | /// respects this mechanism, only one full TLS message will be buffered by pyrtls. 110 | fn readable(&self) -> bool { 111 | self.inner.wants_read() 112 | } 113 | 114 | /// Read TLS content from `buf` into the internal buffer. Return the number of bytes read, or 115 | /// `0` once a `close_notify` alert has been received. No additional data is read in this 116 | /// state. 117 | /// 118 | /// Due to internal buffering, `buf` may contain TLS messages in arbitrary-sized chunks (like 119 | /// a socket or pipe might). 120 | /// 121 | /// You should call `process_new_packets()` each time a call to this function succeeds in 122 | /// order to empty the incoming TLS data buffer. 123 | /// 124 | /// Exceptions may be raised to signal backpressure: 125 | /// 126 | /// * In order to empty the incoming TLS data buffer, you should call `process_new_packets()` 127 | /// each time a call to this function succeeds. 128 | /// * In order to empty the incoming plaintext data buffer, you should call `read_into()` 129 | /// after the call to `process_new_packets()`. 130 | /// 131 | /// You should call `process_new_packets()` each time a call to this function succeeds in 132 | /// order to empty the incoming TLS data buffer 133 | /// 134 | /// Mirrors the `RawIO.write()` interface. 135 | fn read_tls(&mut self, buf: &[u8]) -> PyResult { 136 | Ok(self 137 | .inner 138 | .read_tls(&mut Cursor::new(buf)) 139 | .map_err(TlsError::from)?) 140 | } 141 | 142 | /// Processes any new packets read by a previous call to `read_tls()`. 143 | /// 144 | /// Errors from this function relate to TLS protocol errors, and are fatal to the connection. 145 | /// Future calls after an error will do no new work and will return the same error. After an 146 | /// error is returned from `process_new_packets()`, you should not call `read_tls()` anymore 147 | /// (it will fill up buffers to no purpose). However, you may call the other methods on the 148 | /// connection, including `write()` and `write_tls_into()`. Most likely you will want to 149 | /// call `write_tls_into()` to send any alerts queued by the error and then close the 150 | /// underlying connection. 151 | /// 152 | /// In case of success, yields an `IoState` object with sundry state about the connection. 153 | fn process_new_packets(&mut self) -> PyResult { 154 | Ok(self 155 | .inner 156 | .process_new_packets() 157 | .map_err(TlsError::from)? 158 | .into()) 159 | } 160 | 161 | /// Send the plaintext `buf` to the peer, encrypting and authenticating it. Once this 162 | /// function succeeds you should call `write_tls_into()` which will output the 163 | /// corresponding TLS records. 164 | /// 165 | /// This function buffers plaintext sent before the TLS handshake completes, and sends it as 166 | /// soon as it can. 167 | fn write(&mut self, buf: &[u8]) -> PyResult { 168 | Ok(self.inner.writer().write(buf).map_err(TlsError::from)?) 169 | } 170 | 171 | /// Returns `True` if the caller should call `write_tls_into()` as soon as possible. 172 | fn writable(&self) -> bool { 173 | self.inner.wants_write() 174 | } 175 | 176 | /// Writes TLS messages into `buf`. 177 | /// 178 | /// On success, this function returns the number of bytes written (after encoding and 179 | /// encryption). 180 | /// 181 | /// After this function returns, the connection buffer may not yet be fully flushed. 182 | /// `writable()` can be used to check if the output buffer is empty. 183 | /// 184 | /// Mirrors the `RawIO.readinto()` interface. 185 | fn write_tls_into(&mut self, buf: &Bound<'_, PyByteArray>) -> PyResult { 186 | let mut buf = unsafe { buf.as_bytes_mut() }; 187 | Ok(self.inner.write_tls(&mut buf).map_err(TlsError::from)?) 188 | } 189 | 190 | /// Obtain plaintext data received from the peer over this TLS connection. 191 | /// 192 | /// If the peer closes the TLS connection cleanly, this returns `0` once all the pending data 193 | /// has been read. No further data can be received on that connection, so the underlying TCP 194 | /// connection should be half-closed too. 195 | /// 196 | /// If the peer closes the TLS connection uncleanly (a TCP EOF without sending a 197 | /// `close_notify` alert) this functions raises a `TlsError` exception once any pending data 198 | /// has been read. 199 | /// 200 | /// Note that support for `close_notify` varies in peer TLS libraries: many do not support it 201 | /// and uncleanly close the TCP connection (this might be vulnerable to truncation attacks 202 | /// depending on the application protocol). This means applications using pyrtls must both 203 | /// handle EOF from this function, **and** unexpected EOF of the underlying TCP connection. 204 | /// 205 | /// If there are no bytes to read, this raises a `TlsError` exception. 206 | /// 207 | /// You may learn the number of bytes available at any time by inspecting the return of 208 | /// `process_new_packets()`. 209 | fn read_into(&mut self, buf: &Bound<'_, PyByteArray>) -> PyResult { 210 | let buf = unsafe { buf.as_bytes_mut() }; 211 | Ok(self.inner.reader().read(buf).map_err(TlsError::from)?) 212 | } 213 | } 214 | 215 | /// Create a new `ClientConfig` object (similar to `ssl.SSLContext`). A new `ClientConnection` can 216 | /// only be created by passing in a reference to a `ClientConfig` object. 217 | /// 218 | /// The most important configuration for `ClientConfig` is the certificate verification process. 219 | /// Three different options are offered to define the desired process: 220 | /// 221 | /// - `platform_verifier` (enabled by default) will enable the platform's certificate verifier 222 | /// on platforms that have on, and searching for CA certificates in the system trust store on 223 | /// other platforms (like Linux and FreeBSD). 224 | /// - `mozilla_roots` will enable a built-in set of Mozilla root certificates. This is independent 225 | /// of the operating system, but depends on the pyrtls package to deliver timely updates. 226 | /// - `custom_roots` allows the caller to specify an iterable of trust anchors. Each item must be: 227 | /// - A `TrustAnchor` object, which is a wrapper around a `webpki::TrustAnchor` object 228 | /// - A `bytes` object containing a DER-encoded certificate 229 | /// - A `str` object containing one PEM-encoded certificate 230 | /// 231 | /// The `platform_verifier` option cannot currently be combined with `mozilla_roots` or 232 | /// `custom_roots` (this will raise a `ValueError`), but the latter two can be combined. 233 | /// 234 | /// Other options: 235 | /// 236 | /// - `alpn_protocols` must be an iterable containing `bytes` or `str` objects, each representing 237 | /// one ALPN protocol string. 238 | #[pyclass] 239 | pub(crate) struct ClientConfig { 240 | inner: Arc, 241 | } 242 | 243 | #[pymethods] 244 | impl ClientConfig { 245 | #[new] 246 | #[pyo3(signature = (*, platform_verifier = true, mozilla_roots = false, custom_roots = None, alpn_protocols = None))] 247 | fn new( 248 | platform_verifier: bool, 249 | mozilla_roots: bool, 250 | custom_roots: Option<&Bound<'_, PyAny>>, 251 | alpn_protocols: Option<&Bound<'_, PyAny>>, 252 | ) -> PyResult { 253 | let builder = rustls::ClientConfig::builder(); 254 | let mut config = match (platform_verifier, mozilla_roots, custom_roots) { 255 | (true, false, None) => builder.with_platform_verifier().map_err(TlsError::from)?, 256 | (false, false, None) => { 257 | return Err(PyValueError::new_err("no certificate verifier specified")); 258 | } 259 | (true, _, _) => { 260 | return Err(PyValueError::new_err( 261 | "platform verifier cannot be used with `mozilla_roots` or `custom_roots`", 262 | )); 263 | } 264 | (false, true, custom) | (_, false, custom) => { 265 | let mut roots = RootCertStore::empty(); 266 | if mozilla_roots { 267 | roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); 268 | } 269 | 270 | if let Some(custom) = custom { 271 | for obj in custom.try_iter()? { 272 | let obj = obj?; 273 | if let Ok(ta) = obj.extract::() { 274 | roots.extend([ta.inner].into_iter()) 275 | } else if let Ok(ta) = py_to_cert_der(&obj) { 276 | let (added, _) = roots.add_parsable_certificates([ta]); 277 | if added != 1 { 278 | return Err(PyValueError::new_err( 279 | "unable to parse trust anchor from DER", 280 | )); 281 | } 282 | } else if let Ok(cert_der) = py_to_pem::(&obj) { 283 | if roots.add(cert_der).is_err() { 284 | return Err(PyValueError::new_err( 285 | "unable to parse trust anchor from PEM", 286 | )); 287 | } 288 | } 289 | } 290 | } 291 | 292 | builder.with_root_certificates(roots) 293 | } 294 | } 295 | .with_no_client_auth(); 296 | 297 | config.alpn_protocols = extract_alpn_protocols(alpn_protocols)?; 298 | Ok(Self { 299 | inner: Arc::new(config), 300 | }) 301 | } 302 | 303 | /// Use the `ClientConfig` and the given `sock` to create a `ClientSocket`. 304 | /// 305 | /// Returns a `ClientSocket` if successful. Raises a `ValueError` if `server_hostname` 306 | /// is not a valid server name (either a DNS name or an IP address). 307 | #[pyo3(signature = (sock, server_hostname, do_handshake_on_connect=true))] 308 | fn wrap_socket( 309 | &self, 310 | sock: &Bound<'_, PyAny>, 311 | server_hostname: &Bound<'_, PyString>, 312 | do_handshake_on_connect: bool, 313 | ) -> PyResult { 314 | let hostname = match ServerName::try_from(server_hostname.to_str()?) { 315 | Ok(n) => n.to_owned(), 316 | Err(_) => return Err(PyValueError::new_err("invalid hostname")), 317 | }; 318 | 319 | let conn = match rustls::ClientConnection::new(self.inner.clone(), hostname) { 320 | Ok(conn) => conn, 321 | Err(err) => { 322 | return Err(PyException::new_err(format!( 323 | "failed to initialize ClientConnection: {err}" 324 | ))) 325 | } 326 | }; 327 | 328 | Ok(ClientSocket { 329 | state: SessionState::new(sock, conn)?, 330 | do_handshake_on_connect, 331 | }) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::io::{self, Read, Write}; 3 | use std::ops::{Deref, DerefMut}; 4 | #[cfg(unix)] 5 | use std::os::unix::io::{FromRawFd, RawFd}; 6 | #[cfg(windows)] 7 | use std::os::windows::io::{FromRawSocket, RawSocket}; 8 | 9 | use pyo3::exceptions::{PyTypeError, PyValueError}; 10 | use pyo3::types::{ 11 | PyAnyMethods, PyBytes, PyBytesMethods, PyModule, PyModuleMethods, PyString, PyStringMethods, 12 | }; 13 | use pyo3::{pyclass, pymethods, pymodule, Bound, PyAny, PyErr, PyResult, Python}; 14 | use rustls::pki_types::pem::PemObject; 15 | use rustls::pki_types::{self, CertificateDer, PrivateKeyDer}; 16 | use rustls::ConnectionCommon; 17 | use socket2::Socket; 18 | 19 | mod client; 20 | use client::{ClientConfig, ClientConnection, ClientSocket}; 21 | mod server; 22 | use server::{ServerConfig, ServerConnection, ServerSocket}; 23 | 24 | #[pymodule] 25 | fn pyrtls(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 26 | m.add_class::()?; 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | m.add_class::()?; 33 | m.add_class::()?; 34 | m.add_class::()?; 35 | Ok(()) 36 | } 37 | 38 | struct SessionState { 39 | socket: Socket, 40 | conn: C, 41 | read_buf: Vec, 42 | readable: usize, 43 | user_buf: Vec, 44 | blocking: bool, 45 | } 46 | 47 | impl SessionState 48 | where 49 | C: Deref> + DerefMut, 50 | { 51 | fn new(sock: &Bound<'_, PyAny>, conn: C) -> PyResult { 52 | let blocking = sock.call_method0("getblocking")?.extract::()?; 53 | 54 | #[cfg(unix)] 55 | let socket = match sock.call_method0("detach")?.extract::()? { 56 | -1 => return Err(PyValueError::new_err("invalid file descriptor number")), 57 | fd => unsafe { Socket::from_raw_fd(fd) }, 58 | }; 59 | 60 | #[cfg(windows)] 61 | let socket = match sock.call_method0("detach")?.extract::()? { 62 | // TODO: no clue how Windows expresses an error here? 63 | fd => unsafe { Socket::from_raw_socket(fd) }, 64 | }; 65 | 66 | Ok(Self { 67 | socket, 68 | conn, 69 | read_buf: vec![0; 16_384], 70 | readable: 0, 71 | user_buf: vec![0; 4_096], 72 | blocking, 73 | }) 74 | } 75 | 76 | fn do_handshake(&mut self) -> PyResult<()> { 77 | let _ = self.conn.complete_io(&mut self.socket)?; 78 | Ok(()) 79 | } 80 | 81 | fn send(&mut self, bytes: &Bound<'_, PyBytes>) -> PyResult { 82 | let written = self.conn.writer().write(bytes.as_bytes())?; 83 | let _ = self.conn.complete_io(&mut self.socket)?; 84 | Ok(written) 85 | } 86 | 87 | fn recv<'p>(&mut self, size: usize, py: Python<'p>) -> PyResult> { 88 | self.read(py)?; 89 | if self.user_buf.len() < size { 90 | self.user_buf.resize_with(size, || 0); 91 | } 92 | 93 | let read = if self.blocking { 94 | loop { 95 | match self.conn.reader().read(&mut self.user_buf[..size]) { 96 | Ok(n) => break n, 97 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => { 98 | py.check_signals()?; 99 | self.read(py)?; 100 | continue; 101 | } 102 | Err(e) => return Err(e.into()), 103 | } 104 | } 105 | } else { 106 | self.conn.reader().read(&mut self.user_buf[..size])? 107 | }; 108 | 109 | Ok(PyBytes::new(py, &self.user_buf[..read])) 110 | } 111 | 112 | fn read(&mut self, py: Python<'_>) -> PyResult<()> { 113 | if self.readable < self.read_buf.len() { 114 | self.readable += if self.blocking { 115 | loop { 116 | match self.socket.read(&mut self.read_buf[self.readable..]) { 117 | Ok(n) => break n, 118 | Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { 119 | py.check_signals()?; 120 | continue; 121 | } 122 | Err(e) => return Err(e.into()), 123 | } 124 | } 125 | } else { 126 | self.socket.read(&mut self.read_buf[self.readable..])? 127 | } 128 | } 129 | 130 | if self.conn.wants_read() { 131 | let read = self.conn.read_tls(&mut &self.read_buf[..self.readable])?; 132 | self.read_buf.copy_within(read..self.readable, 0); 133 | self.readable -= read; 134 | if read > 0 { 135 | if let Err(e) = self.conn.process_new_packets() { 136 | return Err(PyValueError::new_err(format!("error: {e}"))); 137 | } 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | } 144 | 145 | /// A `TrustAnchor` represents a trusted authority for verifying certificates. These are used 146 | /// to verify the chain of trust from a certificate to a trusted root for server certificates. 147 | /// All arguments must be `bytes` instances containing DER-encoded data. 148 | #[pyclass] 149 | #[derive(Clone)] 150 | struct TrustAnchor { 151 | inner: pki_types::TrustAnchor<'static>, 152 | } 153 | 154 | #[pymethods] 155 | impl TrustAnchor { 156 | #[new] 157 | #[pyo3(signature = (subject, subject_public_key_info, name_constraints=None))] 158 | fn new( 159 | subject: &Bound<'_, PyBytes>, 160 | subject_public_key_info: &Bound<'_, PyBytes>, 161 | name_constraints: Option<&Bound<'_, PyBytes>>, 162 | ) -> Self { 163 | Self { 164 | inner: pki_types::TrustAnchor { 165 | subject: subject.as_bytes().into(), 166 | subject_public_key_info: subject_public_key_info.as_bytes().into(), 167 | name_constraints: name_constraints.map(|nc| nc.as_bytes().into()), 168 | } 169 | .to_owned(), 170 | } 171 | } 172 | } 173 | 174 | #[pyclass] 175 | struct IoState { 176 | inner: rustls::IoState, 177 | } 178 | 179 | #[pymethods] 180 | impl IoState { 181 | /// How many bytes could be written by `Connection.write_tls_into()` if called right now. 182 | /// A non-zero value implies that `Connection.wants_write()` would yield `True`. 183 | #[getter] 184 | fn tls_bytes_to_write(&self) -> usize { 185 | self.inner.tls_bytes_to_write() 186 | } 187 | 188 | /// How many plaintext bytes are currently buffered in the connection. 189 | #[getter] 190 | fn plaintext_bytes_to_read(&self) -> usize { 191 | self.inner.plaintext_bytes_to_read() 192 | } 193 | 194 | /// `True` if the peer has sent us a `close_notify` alert. This is the TLS mechanism to 195 | /// securely half-close a TLS connection, and signifies that the peer will not send any 196 | /// further data on this connection. 197 | #[getter] 198 | fn peer_has_closed(&self) -> bool { 199 | self.inner.peer_has_closed() 200 | } 201 | } 202 | 203 | impl From for IoState { 204 | fn from(value: rustls::IoState) -> Self { 205 | Self { inner: value } 206 | } 207 | } 208 | 209 | /// `TLSError` represents any errors coming from pyrtls. Its string representation 210 | /// contains more detailed error information. 211 | #[pyclass(name = "TLSError")] 212 | struct TlsError { 213 | inner: Box, 214 | } 215 | 216 | impl From for TlsError { 217 | fn from(e: E) -> Self { 218 | Self { inner: Box::new(e) } 219 | } 220 | } 221 | 222 | impl From for PyErr { 223 | fn from(e: TlsError) -> Self { 224 | PyValueError::new_err(format!("error: {}", e.inner)) 225 | } 226 | } 227 | 228 | fn extract_alpn_protocols(iter: Option<&Bound<'_, PyAny>>) -> PyResult>> { 229 | let mut alpn = Vec::with_capacity(match iter { 230 | Some(ap) => ap.len()?, 231 | None => 0, 232 | }); 233 | 234 | if let Some(protos) = iter { 235 | for proto in protos.try_iter()? { 236 | let proto = proto?; 237 | if let Ok(proto) = proto.downcast_exact::() { 238 | alpn.push(proto.as_bytes().to_vec()); 239 | } else if let Ok(proto) = proto.downcast_exact::() { 240 | alpn.push(proto.to_str()?.as_bytes().to_vec()); 241 | } else { 242 | return Err(PyTypeError::new_err("invalid type for ALPN protocol")); 243 | } 244 | } 245 | } 246 | 247 | Ok(alpn) 248 | } 249 | 250 | fn py_to_pem(obj: &Bound<'_, PyAny>) -> PyResult { 251 | let pem = obj.downcast_exact::()?.to_str()?; 252 | match T::from_pem_slice(pem.as_bytes()) { 253 | Ok(obj) => Ok(obj), 254 | Err(err) => Err(TlsError::from(err).into()), 255 | } 256 | } 257 | 258 | fn py_to_cert_der<'a>(obj: &'a Bound<'a, PyAny>) -> PyResult> { 259 | let der = obj.downcast_exact::()?.as_bytes(); 260 | if der.starts_with(b"-----") { 261 | return Err(PyValueError::new_err("PEM data passed as bytes object")); 262 | } 263 | 264 | Ok(CertificateDer::from(der)) 265 | } 266 | 267 | fn py_to_key_der<'a>(obj: &'a Bound<'a, PyAny>) -> PyResult> { 268 | let der = obj.downcast_exact::()?.as_bytes(); 269 | if der.starts_with(b"-----") { 270 | return Err(PyValueError::new_err("PEM data passed as bytes object")); 271 | } 272 | 273 | PrivateKeyDer::try_from(der) 274 | .map_err(|err| PyValueError::new_err(format!("error parsing private key: {err}"))) 275 | } 276 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read, Write}; 2 | use std::net::ToSocketAddrs; 3 | use std::sync::Arc; 4 | 5 | use pyo3::exceptions::{PyException, PyValueError}; 6 | use pyo3::types::{ 7 | PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes, PyTuple, PyTupleMethods, 8 | }; 9 | use pyo3::{pyclass, pymethods, Bound, PyAny, PyResult, Python}; 10 | use rustls::pki_types::{CertificateDer, PrivateKeyDer}; 11 | 12 | use crate::{ 13 | extract_alpn_protocols, py_to_cert_der, py_to_key_der, py_to_pem, IoState, SessionState, 14 | TlsError, 15 | }; 16 | 17 | /// A `ServerSocket` is a wrapper type that contains both a `socket.socket` and a 18 | /// `ServerConnection` object. It is similar to the `ssl.SSLSocket` class from the 19 | /// standard library and should implement most of the same methods. 20 | #[pyclass] 21 | pub(crate) struct ServerSocket { 22 | state: SessionState, 23 | } 24 | 25 | #[pymethods] 26 | impl ServerSocket { 27 | /// Bind to the given `address`. `address` must currently be a 2-element tuple 28 | /// containing a hostname and a port number. 29 | fn bind(&mut self, address: &Bound<'_, PyTuple>) -> PyResult<()> { 30 | if address.len() != 2 { 31 | return Err(PyValueError::new_err( 32 | "only 2-element address tuples are supported", 33 | )); 34 | } 35 | 36 | let host = address.get_item(0)?; 37 | let host = host.extract::<&str>()?; 38 | let port = address.get_item(1)?.extract::()?; 39 | let addr = match (host, port).to_socket_addrs()?.next() { 40 | Some(addr) => addr, 41 | None => { 42 | return Err(PyValueError::new_err( 43 | "unable to convert address to socket address", 44 | )) 45 | } 46 | }; 47 | 48 | self.state.socket.bind(&addr.into())?; 49 | Ok(()) 50 | } 51 | 52 | /// Perform the TLS setup handshake. 53 | fn do_handshake(&mut self) -> PyResult<()> { 54 | self.state.do_handshake() 55 | } 56 | 57 | /// Send data to the socket. The socket must be connected to a remote socket. Returns the 58 | /// number of bytes sent. Applications are responsible for checking that all data has been 59 | /// sent; if only some of the data was transmitted, the application needs to attempt delivery 60 | /// of the remaining data. 61 | fn send(&mut self, bytes: &Bound<'_, PyBytes>) -> PyResult { 62 | self.state.send(bytes) 63 | } 64 | 65 | /// Receive data from the socket. The return value is a bytes object representing the data 66 | /// received. The maximum amount of data to be received at once is specified by `size`. 67 | /// A returned empty bytes object indicates that the client has disconnected. 68 | fn recv<'p>(&mut self, size: usize, py: Python<'p>) -> PyResult> { 69 | self.state.recv(size, py) 70 | } 71 | } 72 | 73 | /// A `ServerConnection` contains TLS state associated with a single server-side connection. 74 | /// It does not contain any networking state, and is not directly associated with a socket, 75 | /// so I/O happens via the methods on this object directly. 76 | /// 77 | /// A `ServerConnection` can be created from a `ServerConfig` `config`. 78 | #[pyclass] 79 | pub(crate) struct ServerConnection { 80 | inner: rustls::ServerConnection, 81 | } 82 | 83 | #[pymethods] 84 | impl ServerConnection { 85 | #[new] 86 | fn new(config: &ServerConfig) -> PyResult { 87 | Ok(Self { 88 | inner: rustls::ServerConnection::new(config.inner.clone()).map_err(TlsError::from)?, 89 | }) 90 | } 91 | 92 | /// Returns `true` if the caller should call `read_tls()` as soon as possible. 93 | /// 94 | /// If there is pending plaintext data to read, this returns `false`. If your application 95 | /// respects this mechanism, only one full TLS message will be buffered by pyrtls. 96 | fn readable(&self) -> bool { 97 | self.inner.wants_read() 98 | } 99 | 100 | /// Read TLS content from `buf` into the internal buffer. Return the number of bytes read, or 101 | /// `0` once a `close_notify` alert has been received. No additional data is read in this 102 | /// state. 103 | /// 104 | /// Due to internal buffering, `buf` may contain TLS messages in arbitrary-sized chunks (like 105 | /// a socket or pipe might). 106 | /// 107 | /// You should call `process_new_packets()` each time a call to this function succeeds in 108 | /// order to empty the incoming TLS data buffer. 109 | /// 110 | /// Exceptions may be raised to signal backpressure: 111 | /// 112 | /// * In order to empty the incoming TLS data buffer, you should call `process_new_packets()` 113 | /// each time a call to this function succeeds. 114 | /// * In order to empty the incoming plaintext data buffer, you should call `read_into()` 115 | /// after the call to `process_new_packets()`. 116 | /// 117 | /// You should call `process_new_packets()` each time a call to this function succeeds in 118 | /// order to empty the incoming TLS data buffer 119 | /// 120 | /// Mirrors the `RawIO.write()` interface. 121 | fn read_tls(&mut self, buf: &[u8]) -> PyResult { 122 | Ok(self 123 | .inner 124 | .read_tls(&mut Cursor::new(buf)) 125 | .map_err(TlsError::from)?) 126 | } 127 | 128 | /// Processes any new packets read by a previous call to `read_tls()`. 129 | /// 130 | /// Errors from this function relate to TLS protocol errors, and are fatal to the connection. 131 | /// Future calls after an error will do no new work and will return the same error. After an 132 | /// error is returned from `process_new_packets()`, you should not call `read_tls()` anymore 133 | /// (it will fill up buffers to no purpose). However, you may call the other methods on the 134 | /// connection, including `write()` and `write_tls_into()`. Most likely you will want to 135 | /// call `write_tls_into()` to send any alerts queued by the error and then close the 136 | /// underlying connection. 137 | /// 138 | /// In case of success, yields an `IoState` object with sundry state about the connection. 139 | fn process_new_packets(&mut self) -> PyResult { 140 | Ok(self 141 | .inner 142 | .process_new_packets() 143 | .map_err(TlsError::from)? 144 | .into()) 145 | } 146 | 147 | /// Send the plaintext `buf` to the peer, encrypting and authenticating it. Once this 148 | /// function succeeds you should call `write_tls_into()` which will output the 149 | /// corresponding TLS records. 150 | /// 151 | /// This function buffers plaintext sent before the TLS handshake completes, and sends it as 152 | /// soon as it can. 153 | fn write(&mut self, buf: &[u8]) -> PyResult { 154 | Ok(self.inner.writer().write(buf).map_err(TlsError::from)?) 155 | } 156 | 157 | /// Returns `True` if the caller should call `write_tls_into()` as soon as possible. 158 | fn writable(&self) -> bool { 159 | self.inner.wants_write() 160 | } 161 | 162 | /// Writes TLS messages into `buf`. 163 | /// 164 | /// On success, this function returns the number of bytes written (after encoding and 165 | /// encryption). 166 | /// 167 | /// After this function returns, the connection buffer may not yet be fully flushed. 168 | /// `writable()` can be used to check if the output buffer is empty. 169 | /// 170 | /// Mirrors the `RawIO.readinto()` interface. 171 | fn write_tls_into(&mut self, buf: &Bound<'_, PyByteArray>) -> PyResult { 172 | let mut buf = unsafe { buf.as_bytes_mut() }; 173 | Ok(self.inner.write_tls(&mut buf).map_err(TlsError::from)?) 174 | } 175 | 176 | /// Obtain plaintext data received from the peer over this TLS connection. 177 | /// 178 | /// If the peer closes the TLS connection cleanly, this returns `0` once all the pending data 179 | /// has been read. No further data can be received on that connection, so the underlying TCP 180 | /// connection should be half-closed too. 181 | /// 182 | /// If the peer closes the TLS connection uncleanly (a TCP EOF without sending a 183 | /// `close_notify` alert) this functions raises a `TlsError` exception once any pending data 184 | /// has been read. 185 | /// 186 | /// Note that support for `close_notify` varies in peer TLS libraries: many do not support it 187 | /// and uncleanly close the TCP connection (this might be vulnerable to truncation attacks 188 | /// depending on the application protocol). This means applications using pyrtls must both 189 | /// handle EOF from this function, **and** unexpected EOF of the underlying TCP connection. 190 | /// 191 | /// If there are no bytes to read, this raises a `TlsError` exception. 192 | /// 193 | /// You may learn the number of bytes available at any time by inspecting the return of 194 | /// `process_new_packets()`. 195 | fn read_into(&mut self, buf: &Bound<'_, PyByteArray>) -> PyResult { 196 | let buf = unsafe { buf.as_bytes_mut() }; 197 | Ok(self.inner.reader().read(buf).map_err(TlsError::from)?) 198 | } 199 | } 200 | 201 | /// Create a new `ServerConfig` object (similar to `ssl.SSLContext`). A new `ServerConnection` can 202 | /// only be created by passing in a reference to a `ServerConfig` object. 203 | /// 204 | /// The important configuration for `ServerConfig` is the certificate to supply to connecting 205 | /// clients, and the private key used to prove ownership of the certificate. 206 | /// 207 | /// Positional (mandatory) arguments: 208 | /// 209 | /// - `cert_chain`: an iterable, where each value must be of type `bytes` (representing the 210 | /// certificate encoded in DER) or `str` (with the certificate encoded in PEM). 211 | /// - `private_key`: a `bytes` or `str` object, containing the private key encoded in DER or PEM 212 | /// respectively. The private key can be in PKCS#1, PKCS#8, or SEC1 format. 213 | /// 214 | /// Other options: 215 | /// 216 | /// - `alpn_protocols` must be an iterable containing `bytes` or `str` objects, each representing 217 | /// one ALPN protocol string. 218 | #[pyclass] 219 | pub(crate) struct ServerConfig { 220 | inner: Arc, 221 | } 222 | 223 | #[pymethods] 224 | impl ServerConfig { 225 | #[new] 226 | #[pyo3(signature = (cert_chain, private_key, *, alpn_protocols = None))] 227 | fn new( 228 | cert_chain: &Bound<'_, PyAny>, 229 | private_key: &Bound<'_, PyAny>, 230 | alpn_protocols: Option<&Bound<'_, PyAny>>, 231 | ) -> PyResult { 232 | let mut certs = Vec::new(); 233 | for cert in cert_chain.try_iter()? { 234 | let cert = cert?; 235 | if let Ok(cert_der) = py_to_cert_der(&cert) { 236 | certs.push(cert_der.into_owned()); 237 | continue; 238 | } 239 | 240 | certs.push(py_to_pem::(&cert)?); 241 | } 242 | 243 | let key = match py_to_key_der(private_key) { 244 | Ok(key_der) => key_der.clone_key(), 245 | Err(_) => py_to_pem::(private_key)?, 246 | }; 247 | 248 | let mut config = rustls::ServerConfig::builder() 249 | .with_no_client_auth() 250 | .with_single_cert(certs, key) 251 | .map_err(|err| { 252 | PyException::new_err(format!("error initializing ServerConfig: {err}")) 253 | })?; 254 | config.alpn_protocols = extract_alpn_protocols(alpn_protocols)?; 255 | 256 | Ok(Self { 257 | inner: Arc::new(config), 258 | }) 259 | } 260 | 261 | /// Use the `ServerConfig` and the given `sock` to create a `ServerSocket`. 262 | /// 263 | /// Returns a `ServerSocket` if successful. 264 | fn wrap_socket(&self, sock: &Bound<'_, PyAny>) -> PyResult { 265 | let conn = match rustls::ServerConnection::new(self.inner.clone()) { 266 | Ok(conn) => conn, 267 | Err(err) => { 268 | return Err(PyException::new_err(format!( 269 | "failed to initialize ServerConnection: {err}" 270 | ))) 271 | } 272 | }; 273 | 274 | Ok(ServerSocket { 275 | state: SessionState::new(sock, conn)?, 276 | }) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import pyrtls 2 | import socket 3 | import sys 4 | 5 | def main(): 6 | print(pyrtls.__file__) 7 | client_config = pyrtls.ClientConfig() 8 | sock = socket.socket() 9 | sock = client_config.wrap_socket(sock, 'xavamedia.nl') 10 | sock.connect(('xavamedia.nl', 443)) 11 | print(sock.send(b'GET / HTTP/1.0\r\nHost: xavamedia.nl\r\nConnection: close\r\n\r\n')) 12 | print(repr(sock.recv(8192))) 13 | 14 | def echo_server(): 15 | with open('tests/ee-certificate.pem', 'r') as f: 16 | cert_pem = f.read() 17 | with open('tests/ee-key.pem', 'r') as f: 18 | key_pem = f.read() 19 | 20 | server_config = pyrtls.ServerConfig([cert_pem], key_pem) 21 | listener = socket.socket() 22 | listener.bind(('0.0.0.0', 8192)) 23 | listener.listen() 24 | sock, addr = listener.accept() 25 | sock = server_config.wrap_socket(sock) 26 | 27 | req = sock.recv(5) 28 | print(repr(req)) 29 | print(sock.send(req)) 30 | 31 | def echo_client(): 32 | client_config = pyrtls.ClientConfig() 33 | sock = socket.socket() 34 | sock = client_config.wrap_socket(sock, 'localhost') 35 | sock.connect(('127.0.0.1', 8192)) 36 | print(sock.send('HELLO')) 37 | print(repr(sock.recv(5))) 38 | 39 | if __name__ == '__main__': 40 | if len(sys.argv) == 1: 41 | main() 42 | elif sys.argv[1] == 'server': 43 | echo_server() 44 | elif sys.argv[1] == 'client': 45 | echo_client() 46 | else: 47 | print('unknown command: {}'.format(sys.argv[1])) 48 | -------------------------------------------------------------------------------- /tests/ca-certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBjjCCATSgAwIBAgIUJ3edkXfaAH896gPZnQFOyFGTz5cwCgYIKoZIzj0EAwIw 3 | ITEfMB0GA1UEAwwWcmNnZW4gc2VsZiBzaWduZWQgY2VydDAgFw03NTAxMDEwMDAw 4 | MDBaGA80MDk2MDEwMTAwMDAwMFowITEfMB0GA1UEAwwWcmNnZW4gc2VsZiBzaWdu 5 | ZWQgY2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOZMDcHLsursnlzyqxus 6 | 2CAUBdv1uv/T8cS6I/hTK5YxnEF/P2ywkd0vbUuYR9TZdzPNOhOdeDd9RkijdUQ4 7 | z1yjSDBGMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAdBgNVHQ4EFgQUJ3edkXfaAH89 8 | 6gPZnQFOyFGTz5cwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiAD 9 | 6enY5VJwOe1sGejgQBOWN+set1sprDd7nI8WnU0m7AIhAPjlqvbyOZadZsR29Zo8 10 | 6HiuWoUpI8+k/5MMsEfkSQ6l 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /tests/ee-certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBXjCCAQWgAwIBAgIVALjeNqqbGWbsAhgOtqNzJISkhZwoMAoGCCqGSM49BAMC 3 | MCExHzAdBgNVBAMMFnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwIBcNNzUwMTAxMDAw 4 | MDAwWhgPNDA5NjAxMDEwMDAwMDBaMCExHzAdBgNVBAMMFnJjZ2VuIHNlbGYgc2ln 5 | bmVkIGNlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQaL8k7GNSrwuECrzbv 6 | dGEGZIMP+/enBbmI/aWPsoYrPWcoUmka16pL0Wf11LN+wzD7COQrfI+X+QyUOMy2 7 | 6SN/oxgwFjAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDRwAwRAIg 8 | ctqpVTp//JF093RLzA7qE+U8LXN8zswGViBDfH2JuEECIEI674GxX9jobyRXm72n 9 | SnfxxFWdgHpN/ZAS7EIehE7n 10 | -----END CERTIFICATE----- 11 | -------------------------------------------------------------------------------- /tests/ee-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOr5AiRHO+MaGelNJ 3 | wdU4vOFVSFhEkyy6YJHCJE28QSehRANCAAQaL8k7GNSrwuECrzbvdGEGZIMP+/en 4 | BbmI/aWPsoYrPWcoUmka16pL0Wf11LN+wzD7COQrfI+X+QyUOMy26SN/ 5 | -----END PRIVATE KEY----- 6 | --------------------------------------------------------------------------------