pqcprobe 0.1.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.
@@ -0,0 +1,24 @@
1
+ # Virtual environments
2
+ .venv/
3
+ venv/
4
+ env/
5
+
6
+ # Python bytecode / caches
7
+ __pycache__/
8
+ *.py[cod]
9
+ *.egg-info/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+
14
+ # Build artifacts
15
+ build/
16
+ dist/
17
+ *.egg
18
+
19
+ # IDE
20
+ .idea/
21
+ .vscode/
22
+
23
+ # OS
24
+ .DS_Store
pqcprobe-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andre Van Klaveren
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.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: pqcprobe
3
+ Version: 0.1.0
4
+ Summary: Probe a server's TLS configuration and post-quantum (ML-KEM) key-exchange readiness
5
+ Project-URL: Homepage, https://github.com/opratr/pqcprobe
6
+ Project-URL: Repository, https://github.com/opratr/pqcprobe
7
+ Project-URL: Issues, https://github.com/opratr/pqcprobe/issues
8
+ Author-email: Andre Van Klaveren <andre@vanklaverens.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: audit,cryptography,harvest-now-decrypt-later,ml-kem,mlkem,post-quantum,pqc,security,ssl,tls
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Security :: Cryptography
24
+ Classifier: Topic :: System :: Networking :: Monitoring
25
+ Requires-Python: >=3.9.2
26
+ Requires-Dist: cryptography>=46.0.0
27
+ Requires-Dist: pyopenssl>=26.3.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: bandit; extra == 'dev'
30
+ Requires-Dist: build; extra == 'dev'
31
+ Requires-Dist: pip-audit; extra == 'dev'
32
+ Requires-Dist: pre-commit; extra == 'dev'
33
+ Requires-Dist: ruff; extra == 'dev'
34
+ Requires-Dist: twine; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # pqcprobe
38
+
39
+ [![Tests](https://github.com/opratr/pqcprobe/actions/workflows/python-tests.yml/badge.svg)](https://github.com/opratr/pqcprobe/actions/workflows/python-tests.yml)
40
+ [![Lint & Security](https://github.com/opratr/pqcprobe/actions/workflows/lint.yml/badge.svg)](https://github.com/opratr/pqcprobe/actions/workflows/lint.yml)
41
+ [![CodeQL](https://github.com/opratr/pqcprobe/actions/workflows/codeql.yml/badge.svg)](https://github.com/opratr/pqcprobe/actions/workflows/codeql.yml)
42
+ [![Python versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/downloads/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
44
+
45
+ A small command-line TLS probing utility that uses pyOpenSSL to inspect an HTTPS server's TLS configuration, with a focus on post-quantum readiness.
46
+
47
+ Features:
48
+ - Reports negotiated TLS version, cipher, ALPN and certificate summary
49
+ - Reports the negotiated key-exchange group and flags whether it is
50
+ post-quantum (e.g. `X25519MLKEM768`) or classical
51
+ - Assesses post-quantum posture: enumerates which key-exchange groups the
52
+ server accepts and flags "harvest-now, decrypt-later" (HNDL) risk when no
53
+ post-quantum key exchange is offered
54
+ - Verifies the certificate matches the requested hostname (SAN/CN, wildcard
55
+ and IP aware)
56
+ - Probes server support for TLS 1.3 and TLS 1.2
57
+ - Samples which TLS 1.2 ciphers the server accepts (and attempts TLS 1.3 ciphersuites where supported by OpenSSL)
58
+ - Can fetch raw PEM for the server certificate (--raw-cert)
59
+ - Concurrency option for probing (--concurrency)
60
+ - Human-friendly summary (--pretty) or JSON output (--json)
61
+ - Meaningful exit codes for scripting (see below)
62
+
63
+ Requirements:
64
+ - Python 3.9.2+
65
+ - pyOpenSSL, cryptography (installed automatically)
66
+ - OpenSSL 3.x recommended (3.5+ for post-quantum group support). On macOS the
67
+ system `openssl` is LibreSSL and cannot test the ML-KEM hybrid groups; install
68
+ OpenSSL 3.5+ (e.g. via Homebrew) for full functionality.
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pip install pqcprobe
74
+ ```
75
+
76
+ This installs a `pqcprobe` command. To run from a source checkout instead:
77
+
78
+ ```bash
79
+ python3 -m venv .venv
80
+ source .venv/bin/activate
81
+ python3 -m pip install -e .
82
+ ```
83
+
84
+ ## Usage
85
+
86
+ ```bash
87
+ pqcprobe https://example.com --pretty
88
+ pqcprobe example.com:443 --json
89
+ pqcprobe example.com:443 --raw-cert
90
+
91
+ # Post-quantum audit: fail (exit 3) if the server offers no PQC key exchange
92
+ pqcprobe https://example.com --fail-on-classical-only
93
+ # Skip group probing entirely (e.g. when the openssl CLI is unavailable)
94
+ pqcprobe https://example.com --no-groups
95
+ ```
96
+
97
+ (From a source checkout without installing, use `python3 pqcprobe.py ...`.)
98
+
99
+ Post-quantum key-exchange probing:
100
+ - Enumerating group support forces individual groups via the native `openssl`
101
+ CLI, since pyOpenSSL does not expose a way to set the group list. OpenSSL 3.5+
102
+ is required for the ML-KEM hybrid groups (`X25519MLKEM768`, etc.). Groups the
103
+ local openssl doesn't recognize are reported as "not testable" rather than
104
+ "unsupported" (relevant on macOS, whose system openssl is LibreSSL).
105
+ - Reading the *negotiated* group uses pyOpenSSL's `Connection.get_group_name()`
106
+ and needs no external tools.
107
+
108
+ Exit codes:
109
+ - `0` success
110
+ - `1` handshake failed
111
+ - `2` certificate hostname mismatch (when verifying)
112
+ - `3` no post-quantum key exchange offered (only with `--fail-on-classical-only`)
113
+
114
+ Notes:
115
+ - Programmatic overriding of TLS 1.3 ciphersuites requires a recent OpenSSL + pyOpenSSL exposing `set_ciphersuites`.
116
+ - Cipher probing may produce handshake failures for many ciphers — the tool records successes and errors.
117
+ - Only use pqcprobe against systems you own or are authorized to test.
118
+
119
+ ## Contributing
120
+
121
+ Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Security
122
+ issues should be reported privately per [SECURITY.md](SECURITY.md).
123
+
124
+ ## License
125
+
126
+ [MIT](LICENSE) © Andre Van Klaveren
127
+
@@ -0,0 +1,91 @@
1
+ # pqcprobe
2
+
3
+ [![Tests](https://github.com/opratr/pqcprobe/actions/workflows/python-tests.yml/badge.svg)](https://github.com/opratr/pqcprobe/actions/workflows/python-tests.yml)
4
+ [![Lint & Security](https://github.com/opratr/pqcprobe/actions/workflows/lint.yml/badge.svg)](https://github.com/opratr/pqcprobe/actions/workflows/lint.yml)
5
+ [![CodeQL](https://github.com/opratr/pqcprobe/actions/workflows/codeql.yml/badge.svg)](https://github.com/opratr/pqcprobe/actions/workflows/codeql.yml)
6
+ [![Python versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/downloads/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ A small command-line TLS probing utility that uses pyOpenSSL to inspect an HTTPS server's TLS configuration, with a focus on post-quantum readiness.
10
+
11
+ Features:
12
+ - Reports negotiated TLS version, cipher, ALPN and certificate summary
13
+ - Reports the negotiated key-exchange group and flags whether it is
14
+ post-quantum (e.g. `X25519MLKEM768`) or classical
15
+ - Assesses post-quantum posture: enumerates which key-exchange groups the
16
+ server accepts and flags "harvest-now, decrypt-later" (HNDL) risk when no
17
+ post-quantum key exchange is offered
18
+ - Verifies the certificate matches the requested hostname (SAN/CN, wildcard
19
+ and IP aware)
20
+ - Probes server support for TLS 1.3 and TLS 1.2
21
+ - Samples which TLS 1.2 ciphers the server accepts (and attempts TLS 1.3 ciphersuites where supported by OpenSSL)
22
+ - Can fetch raw PEM for the server certificate (--raw-cert)
23
+ - Concurrency option for probing (--concurrency)
24
+ - Human-friendly summary (--pretty) or JSON output (--json)
25
+ - Meaningful exit codes for scripting (see below)
26
+
27
+ Requirements:
28
+ - Python 3.9.2+
29
+ - pyOpenSSL, cryptography (installed automatically)
30
+ - OpenSSL 3.x recommended (3.5+ for post-quantum group support). On macOS the
31
+ system `openssl` is LibreSSL and cannot test the ML-KEM hybrid groups; install
32
+ OpenSSL 3.5+ (e.g. via Homebrew) for full functionality.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install pqcprobe
38
+ ```
39
+
40
+ This installs a `pqcprobe` command. To run from a source checkout instead:
41
+
42
+ ```bash
43
+ python3 -m venv .venv
44
+ source .venv/bin/activate
45
+ python3 -m pip install -e .
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```bash
51
+ pqcprobe https://example.com --pretty
52
+ pqcprobe example.com:443 --json
53
+ pqcprobe example.com:443 --raw-cert
54
+
55
+ # Post-quantum audit: fail (exit 3) if the server offers no PQC key exchange
56
+ pqcprobe https://example.com --fail-on-classical-only
57
+ # Skip group probing entirely (e.g. when the openssl CLI is unavailable)
58
+ pqcprobe https://example.com --no-groups
59
+ ```
60
+
61
+ (From a source checkout without installing, use `python3 pqcprobe.py ...`.)
62
+
63
+ Post-quantum key-exchange probing:
64
+ - Enumerating group support forces individual groups via the native `openssl`
65
+ CLI, since pyOpenSSL does not expose a way to set the group list. OpenSSL 3.5+
66
+ is required for the ML-KEM hybrid groups (`X25519MLKEM768`, etc.). Groups the
67
+ local openssl doesn't recognize are reported as "not testable" rather than
68
+ "unsupported" (relevant on macOS, whose system openssl is LibreSSL).
69
+ - Reading the *negotiated* group uses pyOpenSSL's `Connection.get_group_name()`
70
+ and needs no external tools.
71
+
72
+ Exit codes:
73
+ - `0` success
74
+ - `1` handshake failed
75
+ - `2` certificate hostname mismatch (when verifying)
76
+ - `3` no post-quantum key exchange offered (only with `--fail-on-classical-only`)
77
+
78
+ Notes:
79
+ - Programmatic overriding of TLS 1.3 ciphersuites requires a recent OpenSSL + pyOpenSSL exposing `set_ciphersuites`.
80
+ - Cipher probing may produce handshake failures for many ciphers — the tool records successes and errors.
81
+ - Only use pqcprobe against systems you own or are authorized to test.
82
+
83
+ ## Contributing
84
+
85
+ Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Security
86
+ issues should be reported privately per [SECURITY.md](SECURITY.md).
87
+
88
+ ## License
89
+
90
+ [MIT](LICENSE) © Andre Van Klaveren
91
+
@@ -0,0 +1,222 @@
1
+ # Decision Log
2
+
3
+ A running record of notable decisions for pqcprobe — the *why* behind choices
4
+ that the code and commit history don't make obvious on their own. Newest entries
5
+ at the bottom. Each entry: context, the decision, and its status.
6
+
7
+ ---
8
+
9
+ ## 0001 — Purpose: post-quantum readiness auditing (2026-07-03)
10
+
11
+ **Context.** The tool is used to audit our own systems in preparation for
12
+ post-quantum cryptography risk, specifically "harvest-now, decrypt-later"
13
+ (HNDL) exposure.
14
+
15
+ **Decision.** Treat the TLS **key-exchange group** as the primary signal, not
16
+ the symmetric cipher. HNDL risk lives in the key exchange: a session whose keys
17
+ are agreed with classical-only ECDHE can be recorded today and decrypted once a
18
+ cryptographically relevant quantum computer exists. The tool's reporting and
19
+ future features are prioritized accordingly.
20
+
21
+ **Status.** Accepted.
22
+
23
+ ---
24
+
25
+ ## 0005 — Dependency and platform baseline (2026-07-03)
26
+
27
+ **Context.** Dependencies were pinned to older releases, and the CI matrix
28
+ tested Python 3.8 — which neither pinned dependency supports (both require
29
+ Python >= 3.9), so the 3.8 job could never install.
30
+
31
+ **Decision.** Baseline is Python 3.9+ with pyOpenSSL 26.3.0 and cryptography
32
+ 49.0.0 (latest stable as of this date). OpenSSL 3.5+ is recommended because it
33
+ ships native ML-KEM and enables the hybrid `X25519MLKEM768` group by default,
34
+ which the PQC work depends on. CI tests Python 3.9–3.13.
35
+
36
+ **Status.** Accepted.
37
+
38
+ ---
39
+
40
+ ## 0006 — PQC group probing approach (2026-07-03)
41
+
42
+ **Context.** For PQC auditing the tool needs to (a) report the group actually
43
+ negotiated and (b) enumerate which groups a server will accept. We evaluated
44
+ pyOpenSSL 26.3.0's API for this.
45
+
46
+ **Findings.**
47
+ - `Connection.get_group_name()` **is** available — reading the negotiated group
48
+ (e.g. `X25519MLKEM768` vs. a classical `x25519`) is a clean pure-Python call.
49
+ - `set_groups` / `SSL_CTX_set1_groups_list` is **not** bound in pyOpenSSL
50
+ 26.3.0, so we cannot force a specific group list from Python to enumerate
51
+ server support.
52
+
53
+ **Decision.** Report the negotiated group via `get_group_name()`. Enumerate
54
+ server support by shelling out to the native `openssl s_client -groups <group>`
55
+ (OpenSSL 3.5.x supports all ML-KEM hybrids). Flag any server offering only
56
+ classical key exchange as an HNDL risk.
57
+
58
+ **Status.** Implemented on `feature/pqc-group-probing`. Notes from
59
+ implementation: openssl only prints the `Negotiated TLS1.3 group:` line for
60
+ some groups, so handshake success is detected via a real negotiated cipher and
61
+ the forced group is recorded as the one used. Groups the local openssl does not
62
+ recognize are reported as `unknown_locally` (not `unsupported`). Added
63
+ `--fail-on-classical-only` (exit 3) for audit pipelines and `--no-groups` to
64
+ skip probing when the openssl CLI is unavailable.
65
+
66
+ ---
67
+
68
+ ## 0007 — Project name: pqcprobe (2026-07-03)
69
+
70
+ **Context.** Before publishing a public GitHub repo we checked the working names
71
+ for conflicts. `tlsprobe` collides with an archived C tool
72
+ (github.com/marcobellaccini/tlsprobe) and sits near `tls_prober` / `tlsprober`;
73
+ `TLS-Audit`/`tlsaudit` collides with an active, feature-similar Go tool
74
+ (github.com/adedayo/tlsaudit). Both original names are generic and neither
75
+ signals the post-quantum focus that differentiates this tool.
76
+
77
+ **Decision.** Rename the project and CLI to **pqcprobe** ("PQC probe"). It is
78
+ unused on PyPI and GitHub and elsewhere on the web, reads clearly, and escapes
79
+ the crowded generic TLS-scanner namespace. Renamed the module
80
+ (`tlsprobe.py` -> `pqcprobe.py`), test file, and all references.
81
+
82
+ **Follow-up.** The local working directory is still `TLS-Audit`; name the public
83
+ GitHub repo `pqcprobe` at creation for consistency. The PyPI name `pqcprobe` is
84
+ available if we later publish.
85
+
86
+ **Status.** Accepted.
87
+
88
+ ---
89
+
90
+ ## 0008 — Open-source and packaging setup (2026-07-03)
91
+
92
+ **Context.** Preparing the project to accept contributions and be published to
93
+ PyPI. It previously had no license file, contributor docs, or packaging.
94
+
95
+ **Decisions.**
96
+ - **License:** MIT, © 2026 Andre Van Klaveren (added a real `LICENSE` file to
97
+ back the README's existing claim).
98
+ - **Governance:** added `CONTRIBUTING.md`, `SECURITY.md` (private disclosure to
99
+ andre@vanklaverens.com — important for a security tool), `CODE_OF_CONDUCT.md`
100
+ (Contributor Covenant 2.1), and `CHANGELOG.md` (Keep a Changelog).
101
+ - **Packaging:** `pyproject.toml` with the hatchling backend; version is a
102
+ single source of truth read from `__version__` in `pqcprobe.py`; kept the flat
103
+ single-module layout (packaged via `only-include`) rather than moving to
104
+ `src/`, to minimize churn. Exposes a `pqcprobe` console entry point
105
+ (`pqcprobe:main`, which already returned an exit code).
106
+ - **Publishing:** GitHub Actions workflow using PyPI Trusted Publishing (OIDC),
107
+ triggered on a published GitHub Release — no API tokens stored. The actual
108
+ upload is a human-triggered release, never automated blindly.
109
+
110
+ **Follow-ups before first publish.**
111
+ - GitHub namespace is `opratr`; project URLs in `pyproject.toml` and
112
+ `CHANGELOG.md` are set accordingly.
113
+ - Register a PyPI "pending" trusted publisher for project `pqcprobe`
114
+ (workflow `publish.yml`, environment `pypi`).
115
+
116
+ **Status.** Accepted; build validated locally (`python -m build` + `twine
117
+ check` pass, wheel installs and the `pqcprobe` command runs). Not yet uploaded.
118
+
119
+ ---
120
+
121
+ ## 0009 — Linting and security scanning (2026-07-03)
122
+
123
+ **Context.** The project had no linting or static/security analysis — the only
124
+ automated gate was the unit tests. For an OSS security tool that shells out to
125
+ `openssl` and parses untrusted network data, that is a gap.
126
+
127
+ **Decisions.**
128
+ - **Ruff** for linting (rules E/F/W/I/UP), configured in `pyproject.toml` and
129
+ enforced in CI via `ruff check`. `E501` (line length) is left to `ruff format`,
130
+ which is *not* gated — the code is not yet reformatted repo-wide, and a full
131
+ reformat was deliberately avoided to keep diffs reviewable. Import-ordering
132
+ autofixes were applied.
133
+ - **Bandit** for Python SAST, gated in CI at **medium+** severity. The current
134
+ code produces only low-severity findings: `B404` (subprocess import) and ~25
135
+ `B110` (try/except/pass). These are accepted as known/architectural — the
136
+ subprocess call is core functionality (annotated with a safety rationale and
137
+ `# noqa: S603`), and the broad excepts are pre-existing tech debt already
138
+ noted in the original review (candidate for a later cleanup). Gating at
139
+ medium+ keeps CI honest without failing on these.
140
+ - **pip-audit** scans runtime dependencies (`requirements.txt`) for CVEs in CI.
141
+ - **CodeQL** (`github/codeql-action@v4`, `security-and-quality` queries) runs on
142
+ push/PR and weekly.
143
+ - **Dependabot** for the `pip` and `github-actions` ecosystems (weekly), which
144
+ also keeps our "use current versions" preference on autopilot.
145
+ - **pre-commit** config for local Ruff + hygiene hooks (opt-in, not CI-gating).
146
+ - README shows status badges for Tests, Lint & Security, and CodeQL.
147
+
148
+ **Follow-ups.**
149
+ - README badge URLs use the `opratr` namespace; they render once the repo is
150
+ pushed to GitHub.
151
+ - Optional future work: reduce the `B110` broad-except count and then consider
152
+ applying `ruff format` repo-wide and gating it.
153
+
154
+ **Status.** Accepted; all checks pass locally (ruff clean, bandit medium+ clean,
155
+ pip-audit clean, 21 tests pass).
156
+
157
+ ---
158
+
159
+ ## 0010 — Hash-pinned runtime dependencies (2026-07-04)
160
+
161
+ **Context.** For supply-chain integrity we want installs to verify the exact
162
+ artifact, not just the version — protecting against a compromised or substituted
163
+ package on the index.
164
+
165
+ **Decisions.**
166
+ - Hashes live in a **lockfile**, not in package metadata. `pyproject.toml`
167
+ keeps *abstract* version constraints (what `pip install pqcprobe` resolves for
168
+ end users); `requirements.txt` is a fully pinned, hash-locked lockfile of the
169
+ entire transitive tree (cryptography, cffi, pycparser, pyOpenSSL,
170
+ typing-extensions).
171
+ - The lockfile is generated from `requirements.in` with
172
+ `uv pip compile --universal --generate-hashes` — `--universal` embeds
173
+ environment markers so one file is correct across Python 3.9–3.13 (e.g.
174
+ `pycparser` 2.23 for <3.10 vs 3.0 for ≥3.10; `typing-extensions` only <3.13).
175
+ - CI's test job installs with `pip install --require-hashes -r requirements.txt`,
176
+ which fails the build if any artifact's hash does not match (verified locally:
177
+ a tampered hash produces "THESE PACKAGES DO NOT MATCH THE HASHES"). Tests run
178
+ without an editable install because `pqcprobe.py` is importable from the repo
179
+ root; the packaged-install path is validated separately in `publish.yml`.
180
+ - **`requires-python` tightened to `>=3.9.2`**: surfaced during resolution,
181
+ cryptography 49 excludes Python 3.9.0/3.9.1, so the previous `>=3.9` was
182
+ inaccurate.
183
+
184
+ **Notes / limits.**
185
+ - Dev tools and the build backend were initially unpinned; now addressed in
186
+ [[0011]].
187
+ - Dependabot understands hashed requirements and will regenerate hashes on
188
+ updates, so this stays compatible with the "keep versions current" policy.
189
+
190
+ **Status.** Accepted; hash-verified install confirmed in a clean venv, tests
191
+ pass.
192
+
193
+ ---
194
+
195
+ ## 0011 — Hash-pin dev tools and the build backend (2026-07-04)
196
+
197
+ **Context.** Extends [[0010]] to close the residual unpinned surface: the CI dev
198
+ tools (ruff, bandit, pip-audit, build, twine, pre-commit) and the build backend
199
+ (hatchling), which `python -m build` otherwise fetches unpinned in an isolated
200
+ environment.
201
+
202
+ **Decisions.**
203
+ - Added `requirements-dev.in` -> `requirements-dev.txt`, a universal
204
+ hash-locked lockfile for the dev/CI toolchain (same `uv pip compile` flow as
205
+ the runtime lockfile).
206
+ - `lint.yml` installs the tools with `--require-hashes -r requirements-dev.txt`
207
+ instead of an unpinned `pip install`.
208
+ - `publish.yml` installs the same pinned set and runs `python -m build
209
+ --no-isolation`, so the build uses the hash-pinned hatchling rather than
210
+ fetching a build backend on the fly.
211
+ - `hatchling` is included in the dev lockfile specifically to make the
212
+ no-isolation build verifiable.
213
+
214
+ **Notes.**
215
+ - Universal resolution pins per-Python variants where a tool dropped an older
216
+ interpreter (e.g. `bandit` 1.8.6 on 3.9 vs 1.9.4 on ≥3.10); CI (3.13) gets the
217
+ current releases.
218
+ - pre-commit hook repos remain pinned by git `rev` (their native mechanism),
219
+ which is separate from the pip hash lockfiles.
220
+
221
+ **Status.** Accepted; verified in a clean venv — pinned tools run (ruff, bandit,
222
+ pip-audit clean) and `build --no-isolation` + `twine check` pass.