fizzpy 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fizzpy-0.3.0/.github/manylinux-deps.Dockerfile +16 -0
- fizzpy-0.3.0/.github/workflows/check.yml +38 -0
- fizzpy-0.3.0/.github/workflows/publish.yml +31 -0
- fizzpy-0.3.0/.github/workflows/wheels.yml +118 -0
- fizzpy-0.3.0/.gitignore +46 -0
- fizzpy-0.3.0/.vscode/c_cpp_properties.json +19 -0
- fizzpy-0.3.0/.vscode/settings.json +12 -0
- fizzpy-0.3.0/BUILDING.md +57 -0
- fizzpy-0.3.0/CMakeLists.txt +88 -0
- fizzpy-0.3.0/Justfile +35 -0
- fizzpy-0.3.0/LICENSE +21 -0
- fizzpy-0.3.0/PKG-INFO +163 -0
- fizzpy-0.3.0/README.md +136 -0
- fizzpy-0.3.0/_core.cpp +608 -0
- fizzpy-0.3.0/docs/pq-handshake.png +0 -0
- fizzpy-0.3.0/examples/pq_marker.py +43 -0
- fizzpy-0.3.0/fizzpy/__init__.py +302 -0
- fizzpy-0.3.0/fizzpy/_common.py +170 -0
- fizzpy-0.3.0/fizzpy/_core.pyi +116 -0
- fizzpy-0.3.0/fizzpy/_http.py +463 -0
- fizzpy-0.3.0/fizzpy/_transport.py +70 -0
- fizzpy-0.3.0/fizzpy/aio.py +235 -0
- fizzpy-0.3.0/pyproject.toml +127 -0
- fizzpy-0.3.0/pytest.ini +5 -0
- fizzpy-0.3.0/scripts/build_fizz.py +128 -0
- fizzpy-0.3.0/scripts/build_fizz_deps.py +149 -0
- fizzpy-0.3.0/tests/_clienthello.py +34 -0
- fizzpy-0.3.0/tests/conftest.py +347 -0
- fizzpy-0.3.0/tests/test_basic.py +70 -0
- fizzpy-0.3.0/tests/test_extensions.py +62 -0
- fizzpy-0.3.0/tests/test_http.py +369 -0
- fizzpy-0.3.0/tests/test_integration.py +65 -0
- fizzpy-0.3.0/tests/test_keepalive.py +48 -0
- fizzpy-0.3.0/tests/test_redirects.py +93 -0
- fizzpy-0.3.0/tests/test_verify.py +47 -0
- fizzpy-0.3.0/uv.lock +290 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Manylinux base image with the folly + fizz + liboqs tree prebuilt, so wheel
|
|
2
|
+
# builds skip the ~40-min source build and just compile _core against it. Rebuilt
|
|
3
|
+
# by .github/workflows/manylinux-image.yml when build_fizz_deps.py or FIZZ_TAG
|
|
4
|
+
# changes. The dep prefixes land in $FIZZPY_BUILD_INFO/prefix.txt, which CMake
|
|
5
|
+
# reads at configure time (see CMakeLists.txt).
|
|
6
|
+
ARG MANYLINUX_IMAGE=quay.io/pypa/manylinux_2_28_x86_64:latest
|
|
7
|
+
FROM ${MANYLINUX_IMAGE}
|
|
8
|
+
|
|
9
|
+
ARG FIZZ_TAG=v2026.06.08.00
|
|
10
|
+
ENV FIZZ_TAG=${FIZZ_TAG} \
|
|
11
|
+
GETDEPS_SCRATCH=/opt/fizz-deps \
|
|
12
|
+
FIZZPY_BUILD_INFO=/opt/fizzpy-build \
|
|
13
|
+
FIZZ_SRC=/opt/fizz-src
|
|
14
|
+
|
|
15
|
+
COPY scripts/build_fizz_deps.py /opt/build_fizz_deps.py
|
|
16
|
+
RUN python3 /opt/build_fizz_deps.py
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
tags-ignore: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
quality:
|
|
16
|
+
name: Quality
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 10
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v8.2.0
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
|
|
27
|
+
# _core.pyi lets ruff and basedpyright run without the native build.
|
|
28
|
+
- name: Sync toolchain
|
|
29
|
+
run: uv sync --no-install-project --group dev
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: uv run --no-sync ruff check --output-format=github .
|
|
33
|
+
|
|
34
|
+
- name: Format
|
|
35
|
+
run: uv run --no-sync ruff format --check .
|
|
36
|
+
|
|
37
|
+
- name: Type check
|
|
38
|
+
run: uv run --no-sync basedpyright
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
# Reuse the wheel/sdist build so a tag produces exactly the artifacts CI
|
|
9
|
+
# already validates on every push. packages: write lets the deps_image job
|
|
10
|
+
# rebuild the image if a tag introduces a deps change with no prior push.
|
|
11
|
+
build:
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
packages: write
|
|
15
|
+
uses: ./.github/workflows/wheels.yml
|
|
16
|
+
|
|
17
|
+
publish:
|
|
18
|
+
name: Publish to PyPI
|
|
19
|
+
needs: build
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
environment: pypi
|
|
22
|
+
# OIDC token for PyPI Trusted Publishing — no API token/secret required.
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/download-artifact@v8
|
|
27
|
+
with:
|
|
28
|
+
path: dist
|
|
29
|
+
merge-multiple: true
|
|
30
|
+
|
|
31
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
name: Wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
workflow_call:
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
# The folly+fizz+liboqs tree is baked into a GHCR image tagged by the hash of
|
|
17
|
+
# its build inputs. This job rebuilds it only when that hash has no image yet
|
|
18
|
+
# (deps changed, or first run); otherwise it's a fast existence check. Wheels
|
|
19
|
+
# depend on it, so they never run against a missing image.
|
|
20
|
+
deps_image:
|
|
21
|
+
name: Deps image
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
timeout-minutes: 120
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
packages: write
|
|
27
|
+
outputs:
|
|
28
|
+
image: ${{ steps.meta.outputs.image }}
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v6
|
|
31
|
+
|
|
32
|
+
- name: Resolve image ref
|
|
33
|
+
id: meta
|
|
34
|
+
env:
|
|
35
|
+
OWNER: ${{ github.repository_owner }}
|
|
36
|
+
run: |
|
|
37
|
+
repo="ghcr.io/${OWNER,,}/fizz-py-deps"
|
|
38
|
+
tag="${{ hashFiles('scripts/build_fizz_deps.py', '.github/manylinux-deps.Dockerfile') }}"
|
|
39
|
+
echo "repo=$repo" >> "$GITHUB_OUTPUT"
|
|
40
|
+
echo "image=$repo:$tag" >> "$GITHUB_OUTPUT"
|
|
41
|
+
|
|
42
|
+
- name: Log in to GHCR
|
|
43
|
+
uses: docker/login-action@v4
|
|
44
|
+
with:
|
|
45
|
+
registry: ghcr.io
|
|
46
|
+
username: ${{ github.actor }}
|
|
47
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
48
|
+
|
|
49
|
+
- name: Check whether the image already exists
|
|
50
|
+
id: exists
|
|
51
|
+
run: |
|
|
52
|
+
if docker manifest inspect "${{ steps.meta.outputs.image }}" > /dev/null 2>&1; then
|
|
53
|
+
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
54
|
+
else
|
|
55
|
+
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
- name: Build and push deps image
|
|
59
|
+
if: steps.exists.outputs.found == 'false'
|
|
60
|
+
uses: docker/build-push-action@v7
|
|
61
|
+
with:
|
|
62
|
+
context: .
|
|
63
|
+
file: .github/manylinux-deps.Dockerfile
|
|
64
|
+
push: true
|
|
65
|
+
tags: |
|
|
66
|
+
${{ steps.meta.outputs.image }}
|
|
67
|
+
${{ steps.meta.outputs.repo }}:latest
|
|
68
|
+
|
|
69
|
+
build_sdist:
|
|
70
|
+
name: Build sdist
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
timeout-minutes: 10
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v6
|
|
75
|
+
- run: pipx run build --sdist
|
|
76
|
+
- uses: actions/upload-artifact@v7
|
|
77
|
+
with:
|
|
78
|
+
name: sdist
|
|
79
|
+
path: dist/*.tar.gz
|
|
80
|
+
|
|
81
|
+
build_wheels:
|
|
82
|
+
# One job per interpreter, each compiling _core against the prebuilt deps
|
|
83
|
+
# image (minutes, not ~40 min). liboqs travels with fizz's getdeps manifest,
|
|
84
|
+
# giving real post-quantum (X25519MLKEM768) support.
|
|
85
|
+
name: Wheel ${{ matrix.python }}
|
|
86
|
+
needs: deps_image
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
timeout-minutes: 30
|
|
89
|
+
permissions:
|
|
90
|
+
contents: read
|
|
91
|
+
packages: read
|
|
92
|
+
strategy:
|
|
93
|
+
fail-fast: false
|
|
94
|
+
matrix:
|
|
95
|
+
python: [cp310, cp311, cp312, cp313, cp314]
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@v6
|
|
98
|
+
|
|
99
|
+
# cibuildwheel pulls the deps image via docker; authenticate so the GHCR
|
|
100
|
+
# package is reachable.
|
|
101
|
+
- name: Log in to GHCR
|
|
102
|
+
uses: docker/login-action@v4
|
|
103
|
+
with:
|
|
104
|
+
registry: ghcr.io
|
|
105
|
+
username: ${{ github.actor }}
|
|
106
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
107
|
+
|
|
108
|
+
- name: Build wheel
|
|
109
|
+
uses: pypa/cibuildwheel@v4.1.0
|
|
110
|
+
env:
|
|
111
|
+
CIBW_BUILD: ${{ matrix.python }}-*
|
|
112
|
+
CIBW_MANYLINUX_X86_64_IMAGE: ${{ needs.deps_image.outputs.image }}
|
|
113
|
+
|
|
114
|
+
- uses: actions/upload-artifact@v7
|
|
115
|
+
with:
|
|
116
|
+
name: wheels-${{ matrix.python }}
|
|
117
|
+
path: wheelhouse/*.whl
|
|
118
|
+
if-no-files-found: error
|
fizzpy-0.3.0/.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
build/
|
|
2
|
+
CMakeFiles/
|
|
3
|
+
dist/
|
|
4
|
+
.dev/
|
|
5
|
+
_skbuild/
|
|
6
|
+
wheelhouse/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.pyc
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.venv/
|
|
14
|
+
|
|
15
|
+
# Prerequisites
|
|
16
|
+
*.d
|
|
17
|
+
|
|
18
|
+
# Compiled Object files
|
|
19
|
+
*.slo
|
|
20
|
+
*.lo
|
|
21
|
+
*.o
|
|
22
|
+
*.obj
|
|
23
|
+
|
|
24
|
+
# Precompiled Headers
|
|
25
|
+
*.gch
|
|
26
|
+
*.pch
|
|
27
|
+
|
|
28
|
+
# Compiled Dynamic libraries
|
|
29
|
+
*.so
|
|
30
|
+
*.dylib
|
|
31
|
+
*.dll
|
|
32
|
+
|
|
33
|
+
# Fortran module files
|
|
34
|
+
*.mod
|
|
35
|
+
*.smod
|
|
36
|
+
|
|
37
|
+
# Compiled Static libraries
|
|
38
|
+
*.lai
|
|
39
|
+
*.la
|
|
40
|
+
*.a
|
|
41
|
+
*.lib
|
|
42
|
+
|
|
43
|
+
# Executables
|
|
44
|
+
*.exe
|
|
45
|
+
*.out
|
|
46
|
+
*.app
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"configurations": [
|
|
3
|
+
{
|
|
4
|
+
"name": "Linux",
|
|
5
|
+
"includePath": [
|
|
6
|
+
"${workspaceFolder}/**",
|
|
7
|
+
"${workspaceFolder}/build/vcpkg_installed/**",
|
|
8
|
+
"/usr/include/python3.10/**"
|
|
9
|
+
],
|
|
10
|
+
"defines": [],
|
|
11
|
+
"compilerPath": "/usr/bin/clang",
|
|
12
|
+
"cStandard": "c17",
|
|
13
|
+
"cppStandard": "c++20",
|
|
14
|
+
"intelliSenseMode": "linux-clang-x64",
|
|
15
|
+
"configurationProvider": "ms-vscode.cmake-tools"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"version": 4
|
|
19
|
+
}
|
fizzpy-0.3.0/BUILDING.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Building fizzpy
|
|
2
|
+
|
|
3
|
+
The `_core` extension links Facebook's Fizz (C++ TLS 1.3). The post-quantum
|
|
4
|
+
handshake additionally requires a Fizz built against
|
|
5
|
+
[liboqs](https://github.com/open-quantum-safe/liboqs) — the Homebrew bottle and
|
|
6
|
+
vcpkg port both ship `FIZZ_HAVE_OQS=0`, so they negotiate classical groups only.
|
|
7
|
+
|
|
8
|
+
There are two build paths, for two different purposes:
|
|
9
|
+
|
|
10
|
+
| | Local dev (`just build`) | CI wheels (`scripts/build_fizz_deps.py`) |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Fizz/liboqs source | Homebrew + from-source Fizz | getdeps, all from source |
|
|
13
|
+
| Linkage | static Fizz, shared liboqs via rpath | fully static-PIC, self-contained `.so` |
|
|
14
|
+
| Where | your machine | `manylinux_2_28` container |
|
|
15
|
+
| Optimized for | fast iteration | deterministic, portable distribution |
|
|
16
|
+
|
|
17
|
+
## Local development (`just`)
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
brew install fizz liboqs # fizz pulls folly, boost, glog, gflags, libsodium, double-conversion
|
|
21
|
+
pip install -e .[dev] --no-build-isolation # or: pip install scikit-build-core pybind11 pybind11-stubgen ruff pytest
|
|
22
|
+
|
|
23
|
+
just build # build OQS Fizz if needed (cached), then editable-install _core
|
|
24
|
+
just test # offline test suite; `just test ""` includes live network tests
|
|
25
|
+
just check # ruff lint
|
|
26
|
+
just fmt # ruff format + clang-format _core.cpp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`just build` runs `scripts/build_fizz.py ensure`, which compiles a liboqs-enabled
|
|
30
|
+
Fizz into the gitignored `.dev/fizz-prefix` once (subsequent runs are a no-op;
|
|
31
|
+
`python scripts/build_fizz.py ensure --force` rebuilds), then editable-installs
|
|
32
|
+
`_core` with `find_package(fizz)` pointed at that prefix.
|
|
33
|
+
|
|
34
|
+
- **C++20 is required** — current Folly (2026.06) dropped C++17.
|
|
35
|
+
- Fizz and Folly are versioned together. The tag in `scripts/build_fizz.py` must
|
|
36
|
+
match the Homebrew Folly bottle, or the ABI won't match.
|
|
37
|
+
- `find_package(liboqs CONFIG)` inside Fizz auto-sets `FIZZ_HAVE_OQS` and links
|
|
38
|
+
`OQS::oqs`. `BUILD_EXAMPLES=OFF` is required: the bundled FizzTool fails to
|
|
39
|
+
compile under OpenSSL 3.6 (`-Werror` on the deprecated `EVP_PKEY_cmp`); the
|
|
40
|
+
library itself is fine.
|
|
41
|
+
|
|
42
|
+
## Portable wheels (CI)
|
|
43
|
+
|
|
44
|
+
`scripts/build_fizz_deps.py` is cibuildwheel's `before-all`. It builds the whole
|
|
45
|
+
folly + Fizz + liboqs tree once per `manylinux_2_28` container as static-PIC
|
|
46
|
+
archives, which `_core` links in wholesale — a single self-contained extension
|
|
47
|
+
with exactly one copy of folly. (Using getdeps `--shared-lib` instead would bake
|
|
48
|
+
folly into `libfizz.so` *and* drag it into `_core` again, double-registering
|
|
49
|
+
folly's gflags globals and aborting at import.) The build settings live in
|
|
50
|
+
`[tool.cibuildwheel]` in `pyproject.toml`.
|
|
51
|
+
|
|
52
|
+
## Verify
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
python -c "import fizzpy; print(fizzpy.get('https://www.cloudflare.com').tls)"
|
|
56
|
+
# group should be 'X25519MLKEM768', group_code 4588
|
|
57
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15...3.27)
|
|
2
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
3
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
4
|
+
|
|
5
|
+
project(
|
|
6
|
+
${SKBUILD_PROJECT_NAME}
|
|
7
|
+
VERSION ${SKBUILD_PROJECT_VERSION}
|
|
8
|
+
LANGUAGES CXX)
|
|
9
|
+
|
|
10
|
+
# Only Linux x86_64 wheels are published. Stop here before the (~40 min) folly +
|
|
11
|
+
# fizz + liboqs source build that an sdist install would otherwise attempt and
|
|
12
|
+
# fail on macOS/Windows.
|
|
13
|
+
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
|
14
|
+
message(
|
|
15
|
+
FATAL_ERROR
|
|
16
|
+
"fizzpy publishes prebuilt Linux x86_64 wheels only; ${CMAKE_SYSTEM_NAME} "
|
|
17
|
+
"is not yet supported. Building from source needs the Fizz toolchain — see "
|
|
18
|
+
"BUILDING.md (https://github.com/Xevion/fizz-py/blob/master/BUILDING.md).")
|
|
19
|
+
endif()
|
|
20
|
+
|
|
21
|
+
# Fizz's CMake config pulls in its bundled FindSodium.cmake. Homebrew installs
|
|
22
|
+
# it under libexec/cmake; make that discoverable for local (non-vcpkg) builds.
|
|
23
|
+
# Override with -DFIZZ_CMAKE_MODULE_DIR=... if your install differs.
|
|
24
|
+
set(FIZZ_CMAKE_MODULE_DIR "/home/linuxbrew/.linuxbrew/opt/fizz/libexec/cmake"
|
|
25
|
+
CACHE PATH "Directory containing Fizz's bundled CMake find-modules")
|
|
26
|
+
if(EXISTS "${FIZZ_CMAKE_MODULE_DIR}")
|
|
27
|
+
list(APPEND CMAKE_MODULE_PATH "${FIZZ_CMAKE_MODULE_DIR}")
|
|
28
|
+
endif()
|
|
29
|
+
|
|
30
|
+
if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
|
|
31
|
+
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
|
|
32
|
+
CACHE STRING "")
|
|
33
|
+
endif()
|
|
34
|
+
|
|
35
|
+
message(STATUS "Using compiler: ${CMAKE_CXX_COMPILER}")
|
|
36
|
+
|
|
37
|
+
# Portable-wheel builds: scripts/build_fizz_deps.py (cibuildwheel before-all)
|
|
38
|
+
# writes the getdeps install prefixes and fizz's find-module dir here. Read them
|
|
39
|
+
# at configure time rather than as a CIBW environment substitution, which would
|
|
40
|
+
# evaluate before before-all has produced the files. Skipped for local builds,
|
|
41
|
+
# which pass -DCMAKE_PREFIX_PATH directly (see BUILDING.md).
|
|
42
|
+
if(DEFINED ENV{FIZZPY_BUILD_INFO})
|
|
43
|
+
set(_info "$ENV{FIZZPY_BUILD_INFO}")
|
|
44
|
+
if(EXISTS "${_info}/prefix.txt")
|
|
45
|
+
file(STRINGS "${_info}/prefix.txt" _fizz_prefix)
|
|
46
|
+
list(APPEND CMAKE_PREFIX_PATH ${_fizz_prefix})
|
|
47
|
+
endif()
|
|
48
|
+
if(EXISTS "${_info}/module_dir.txt")
|
|
49
|
+
file(STRINGS "${_info}/module_dir.txt" _fizz_modules)
|
|
50
|
+
list(APPEND CMAKE_MODULE_PATH ${_fizz_modules})
|
|
51
|
+
endif()
|
|
52
|
+
endif()
|
|
53
|
+
|
|
54
|
+
# pybind11 is pip-installed (locally, or by before-build in wheel builds); add its
|
|
55
|
+
# CMake config so find_package locates it without a CIBW env substitution.
|
|
56
|
+
# Development.Module must be in this first find_package: pybind11's NewTools skips
|
|
57
|
+
# its own Python discovery once Python_FOUND is set, so python_add_library (from
|
|
58
|
+
# Development.Module) would otherwise never be defined.
|
|
59
|
+
find_package(Python COMPONENTS Interpreter Development.Module QUIET)
|
|
60
|
+
if(Python_Interpreter_FOUND)
|
|
61
|
+
execute_process(
|
|
62
|
+
COMMAND "${Python_EXECUTABLE}" -m pybind11 --cmakedir
|
|
63
|
+
OUTPUT_VARIABLE _pybind11_cmakedir
|
|
64
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
65
|
+
ERROR_QUIET)
|
|
66
|
+
if(_pybind11_cmakedir)
|
|
67
|
+
list(APPEND CMAKE_PREFIX_PATH "${_pybind11_cmakedir}")
|
|
68
|
+
endif()
|
|
69
|
+
endif()
|
|
70
|
+
|
|
71
|
+
set(PYBIND11_NEWPYTHON ON)
|
|
72
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
73
|
+
find_package(fizz CONFIG REQUIRED)
|
|
74
|
+
|
|
75
|
+
pybind11_add_module(_core _core.cpp)
|
|
76
|
+
target_link_libraries(_core PRIVATE fizz::fizz)
|
|
77
|
+
|
|
78
|
+
# _core.cpp drives connections on a folly::ScopedEventBaseThread, but fizz itself
|
|
79
|
+
# never uses that type — so when folly is a static archive (e.g. getdeps
|
|
80
|
+
# --shared-lib portable wheels), its object never makes it into libfizz and the
|
|
81
|
+
# symbol is undefined at import. Link the component directly when folly exposes
|
|
82
|
+
# it as a discrete target; classic shared-folly builds (Homebrew) export it from
|
|
83
|
+
# libfolly already and skip this.
|
|
84
|
+
if(TARGET Folly::folly_io_async_scoped_event_base_thread)
|
|
85
|
+
target_link_libraries(_core PRIVATE Folly::folly_io_async_scoped_event_base_thread)
|
|
86
|
+
endif()
|
|
87
|
+
|
|
88
|
+
install(TARGETS _core LIBRARY DESTINATION fizzpy)
|
fizzpy-0.3.0/Justfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# fizzpy dev tasks. `just build` is the fast local loop (editable _core against
|
|
2
|
+
# a liboqs-enabled Fizz); CI wheels are built separately via scripts/build_fizz_deps.py.
|
|
3
|
+
|
|
4
|
+
_default:
|
|
5
|
+
@just --list
|
|
6
|
+
|
|
7
|
+
# Build the OQS Fizz prefix if needed, then editable-install _core against it.
|
|
8
|
+
build:
|
|
9
|
+
python scripts/build_fizz.py ensure
|
|
10
|
+
pip install -e . --no-build-isolation --config-settings=build-dir=.dev/build-editable --config-settings=cmake.define.CMAKE_PREFIX_PATH="$(python scripts/build_fizz.py prefix-path)"
|
|
11
|
+
|
|
12
|
+
# Build a non-editable wheel and force-reinstall it (full install, not the editable loop).
|
|
13
|
+
wheel:
|
|
14
|
+
python -m build
|
|
15
|
+
pip install ./dist/*.whl --force-reinstall
|
|
16
|
+
|
|
17
|
+
# Run the test suite. Defaults to the offline, deterministic subset; pass args to
|
|
18
|
+
# override, e.g. `just test ""` for the full suite (needs network).
|
|
19
|
+
test *args="-m 'not network'":
|
|
20
|
+
pytest {{args}}
|
|
21
|
+
|
|
22
|
+
# Install the lint/type toolchain into .venv without building _core.
|
|
23
|
+
_tools:
|
|
24
|
+
uv sync --no-install-project --group dev
|
|
25
|
+
|
|
26
|
+
# Lint and type-check Python.
|
|
27
|
+
check: _tools
|
|
28
|
+
uv run --no-sync ruff check .
|
|
29
|
+
uv run --no-sync ruff format --check .
|
|
30
|
+
uv run --no-sync basedpyright
|
|
31
|
+
|
|
32
|
+
# Format Python and C++ in place.
|
|
33
|
+
fmt: _tools
|
|
34
|
+
uv run --no-sync ruff format .
|
|
35
|
+
clang-format -i _core.cpp
|
fizzpy-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xevion
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fizzpy-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fizzpy
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A low-level TLS 1.3 client toolkit for Python, built on Facebook's Fizz
|
|
5
|
+
Author-Email: Xevion <xevion@xevion.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Project-URL: Repository, https://github.com/Xevion/fizz-py
|
|
21
|
+
Project-URL: Issues, https://github.com/Xevion/fizz-py/issues
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: certifi
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# fizzpy
|
|
29
|
+
|
|
30
|
+
A small TLS 1.3 HTTP client for Python, built on Facebook's
|
|
31
|
+
[Fizz](https://github.com/facebookincubator/fizz) (C++) via pybind11.
|
|
32
|
+
|
|
33
|
+
By default it offers a **post-quantum key exchange** — the standardized hybrid
|
|
34
|
+
`X25519MLKEM768` group (codepoint 4588). An ordinary `GET` negotiates a hybrid
|
|
35
|
+
ML-KEM handshake against servers that support it (Cloudflare, Google) and falls
|
|
36
|
+
back to classical X25519 against those that don't.
|
|
37
|
+
|
|
38
|
+
> TLS 1.3 only, HTTP/1.1 only. This is a focused toolkit / learning project,
|
|
39
|
+
> not a drop-in `requests` replacement. See [Status](#status).
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
*A fizzpy request captured in Wireshark: the TLS 1.3 ClientHello offers the
|
|
44
|
+
post-quantum `X25519MLKEM768` group (`0x11ec`), and a custom marker extension
|
|
45
|
+
(`0xFE5A`, [see below](#custom-clienthello-extensions)) makes it unmistakably
|
|
46
|
+
ours — generated by [`examples/pq_marker.py`](examples/pq_marker.py).*
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pip install fizzpy
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The Linux wheels are self-contained: folly, Fizz, and liboqs are statically
|
|
55
|
+
linked into the extension, so a fresh `pip install` has working post-quantum
|
|
56
|
+
TLS with no system packages to add. `certifi` (pulled in automatically) supplies
|
|
57
|
+
the default trust store.
|
|
58
|
+
|
|
59
|
+
Wheels are produced by CI (`manylinux_2_28`, x86_64, CPython 3.10–3.14); if one
|
|
60
|
+
isn't available for your platform yet, build from source — see
|
|
61
|
+
[BUILDING.md](BUILDING.md).
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Synchronous:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import fizzpy
|
|
69
|
+
|
|
70
|
+
r = fizzpy.get("https://www.cloudflare.com")
|
|
71
|
+
print(r.status_code) # 200
|
|
72
|
+
print(r.headers["content-type"]) # text/html; charset=UTF-8
|
|
73
|
+
print(r.text[:64])
|
|
74
|
+
|
|
75
|
+
# The negotiated TLS 1.3 parameters are attached to every response:
|
|
76
|
+
print(r.tls)
|
|
77
|
+
# {'version': 'TLSv1.3', 'cipher': 'TLS_AES_128_GCM_SHA256',
|
|
78
|
+
# 'group': 'X25519MLKEM768', 'group_code': 4588,
|
|
79
|
+
# 'alpn': 'http/1.1', 'sni': 'www.cloudflare.com', 'peer_cert': '...'}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Asynchronous (the same C++ core, resolved on the running event loop):
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
from fizzpy.aio import AsyncClient
|
|
87
|
+
|
|
88
|
+
async def main():
|
|
89
|
+
async with AsyncClient() as client:
|
|
90
|
+
r = await client.get("https://www.google.com")
|
|
91
|
+
print(r.status_code, r.tls["group"], r.tls["group_code"])
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Choosing the key exchange
|
|
97
|
+
|
|
98
|
+
The default offers `[x25519_mlkem768, x25519]`, mirroring how Chrome and Firefox
|
|
99
|
+
send both key shares. Override it per client:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import fizzpy
|
|
103
|
+
from fizzpy import NamedGroup
|
|
104
|
+
|
|
105
|
+
# Force post-quantum only (handshake fails if the server lacks ML-KEM):
|
|
106
|
+
pq = fizzpy.Client(groups=[NamedGroup.x25519_mlkem768])
|
|
107
|
+
|
|
108
|
+
# Classical only:
|
|
109
|
+
classical = fizzpy.Client(groups=[NamedGroup.x25519])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom ClientHello extensions
|
|
113
|
+
|
|
114
|
+
fizzpy surfaces Fizz's low-level extension hook, so you can append arbitrary
|
|
115
|
+
extensions to the TLS 1.3 ClientHello. Each is an opaque `(type, data)` pair;
|
|
116
|
+
fizzpy rejects types it manages itself (key_share, supported_versions, …) and
|
|
117
|
+
out-of-range or duplicate types.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import fizzpy
|
|
121
|
+
|
|
122
|
+
client = fizzpy.Client(
|
|
123
|
+
extensions=[fizzpy.Extension(0xFE5A, b"hello from fizzpy")],
|
|
124
|
+
)
|
|
125
|
+
r = client.get("https://blog.cloudflare.com")
|
|
126
|
+
print(r.tls["group"]) # still negotiates X25519MLKEM768
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The extension is sent verbatim in the ClientHello — see it on the wire with
|
|
130
|
+
[`examples/pq_marker.py`](examples/pq_marker.py) and the screenshot above.
|
|
131
|
+
|
|
132
|
+
### Certificate verification
|
|
133
|
+
|
|
134
|
+
Certificate chains are verified against the [certifi](https://github.com/certifi/python-certifi)
|
|
135
|
+
CA bundle and the hostname is checked against the certificate's SAN by default.
|
|
136
|
+
Point at a custom CA, or disable verification entirely:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
fizzpy.Client(cafile="/path/to/ca.pem") # trust a specific CA
|
|
140
|
+
fizzpy.Client(verify=False) # accept any certificate (insecure)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Status
|
|
144
|
+
|
|
145
|
+
What works today:
|
|
146
|
+
|
|
147
|
+
- TLS 1.3 handshake with classical or post-quantum (ML-KEM) key exchange
|
|
148
|
+
- `GET`/`POST`/`HEAD`/`PUT`/`DELETE`, sync and async
|
|
149
|
+
- HTTP/1.1 response framing (content-length, chunked, gzip/deflate)
|
|
150
|
+
- Connection keep-alive with a per-host pool; automatic redirect following
|
|
151
|
+
- Chain + hostname certificate verification, certifi default + custom CA trust
|
|
152
|
+
|
|
153
|
+
Current limitations:
|
|
154
|
+
|
|
155
|
+
- **TLS 1.3 only** (a Fizz constraint) — it cannot talk to TLS 1.2-only servers
|
|
156
|
+
- HTTP/1.1 only (no HTTP/2)
|
|
157
|
+
|
|
158
|
+
## Building
|
|
159
|
+
|
|
160
|
+
The post-quantum handshake requires a Fizz built against
|
|
161
|
+
[liboqs](https://github.com/open-quantum-safe/liboqs). The CI wheels build the
|
|
162
|
+
whole folly + Fizz + liboqs tree from source inside `manylinux_2_28`; see
|
|
163
|
+
[BUILDING.md](BUILDING.md) for that recipe and for local development builds.
|