kxco-verify 1.0.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,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: kxco-verify
3
+ Version: 1.0.0
4
+ Summary: Receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65 webhook signature scheme
5
+ Author-email: KXCO by Knightsbridge <hello@kxco.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://kxco.ai
8
+ Project-URL: Documentation, https://chain.kxco.ai/wallet/dev-docs
9
+ Project-URL: Repository, https://github.com/JackKXCO/kxco-post-quantum-verifiers
10
+ Keywords: post-quantum,ml-dsa,dilithium,webhook,fips-204,kxco
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Provides-Extra: oqs
17
+ Requires-Dist: liboqs-python>=0.10; extra == "oqs"
18
+ Provides-Extra: pqcrypto
19
+ Requires-Dist: pqcrypto>=0.3; extra == "pqcrypto"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+
23
+ # kxco-verify (Python)
24
+
25
+ Receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65 webhook signature scheme. Wire-format compatible with `@kxco/post-quantum` (npm), the Go verifier, and the Rust verifier.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install kxco-verify # core (HMAC, envelope, fingerprint)
31
+ pip install kxco-verify[oqs] # adds liboqs-python for ML-DSA
32
+ pip install kxco-verify[pqcrypto] # adds pqcrypto for ML-DSA
33
+ ```
34
+
35
+ The HMAC, envelope, fingerprint, and timestamp paths use only the Python standard library. ML-DSA-65 verification is lazy-loaded: it only requires a PQC backend when `verify_pq` is actually called.
36
+
37
+ ## Quick start (FastAPI)
38
+
39
+ ```python
40
+ from fastapi import FastAPI, Request, HTTPException
41
+ import os
42
+ import kxco_verify as kx
43
+
44
+ PINNED_KID = "aa29f37ab7f4b2cf" # current KXCO production kid (refresh from /.well-known if rotated)
45
+ PINNED_PUBKEY = bytes.fromhex("...3904 hex chars...")
46
+ HMAC_SECRET = os.environ["KXCO_WEBHOOK_SECRET"].encode()
47
+
48
+ app = FastAPI()
49
+
50
+ @app.post("/webhooks/kxco")
51
+ async def webhook(request: Request):
52
+ raw_body = await request.body()
53
+ headers = {k.lower(): v for k, v in request.headers.items()}
54
+
55
+ result = kx.verify_delivery(
56
+ headers=headers,
57
+ raw_body=raw_body,
58
+ hmac_secret=HMAC_SECRET,
59
+ pq_public_key=PINNED_PUBKEY,
60
+ pinned_kid=PINNED_KID,
61
+ )
62
+ if not result.ok:
63
+ raise HTTPException(401, "invalid signature")
64
+
65
+ return {"ok": True}
66
+ ```
67
+
68
+ ## Running tests
69
+
70
+ ```bash
71
+ cd python
72
+ python test_kxco_verify.py
73
+ ```
74
+
75
+ Expected: `All 6 vector tests passed.`
76
+
77
+ This verifies that the Python implementation produces identical outputs to `vectors.json` — the same file used by the JavaScript, Go, and Rust verifiers.
78
+
79
+ ## License
80
+
81
+ MIT.
@@ -0,0 +1,59 @@
1
+ # kxco-verify (Python)
2
+
3
+ Receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65 webhook signature scheme. Wire-format compatible with `@kxco/post-quantum` (npm), the Go verifier, and the Rust verifier.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install kxco-verify # core (HMAC, envelope, fingerprint)
9
+ pip install kxco-verify[oqs] # adds liboqs-python for ML-DSA
10
+ pip install kxco-verify[pqcrypto] # adds pqcrypto for ML-DSA
11
+ ```
12
+
13
+ The HMAC, envelope, fingerprint, and timestamp paths use only the Python standard library. ML-DSA-65 verification is lazy-loaded: it only requires a PQC backend when `verify_pq` is actually called.
14
+
15
+ ## Quick start (FastAPI)
16
+
17
+ ```python
18
+ from fastapi import FastAPI, Request, HTTPException
19
+ import os
20
+ import kxco_verify as kx
21
+
22
+ PINNED_KID = "aa29f37ab7f4b2cf" # current KXCO production kid (refresh from /.well-known if rotated)
23
+ PINNED_PUBKEY = bytes.fromhex("...3904 hex chars...")
24
+ HMAC_SECRET = os.environ["KXCO_WEBHOOK_SECRET"].encode()
25
+
26
+ app = FastAPI()
27
+
28
+ @app.post("/webhooks/kxco")
29
+ async def webhook(request: Request):
30
+ raw_body = await request.body()
31
+ headers = {k.lower(): v for k, v in request.headers.items()}
32
+
33
+ result = kx.verify_delivery(
34
+ headers=headers,
35
+ raw_body=raw_body,
36
+ hmac_secret=HMAC_SECRET,
37
+ pq_public_key=PINNED_PUBKEY,
38
+ pinned_kid=PINNED_KID,
39
+ )
40
+ if not result.ok:
41
+ raise HTTPException(401, "invalid signature")
42
+
43
+ return {"ok": True}
44
+ ```
45
+
46
+ ## Running tests
47
+
48
+ ```bash
49
+ cd python
50
+ python test_kxco_verify.py
51
+ ```
52
+
53
+ Expected: `All 6 vector tests passed.`
54
+
55
+ This verifies that the Python implementation produces identical outputs to `vectors.json` — the same file used by the JavaScript, Go, and Rust verifiers.
56
+
57
+ ## License
58
+
59
+ MIT.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: kxco-verify
3
+ Version: 1.0.0
4
+ Summary: Receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65 webhook signature scheme
5
+ Author-email: KXCO by Knightsbridge <hello@kxco.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://kxco.ai
8
+ Project-URL: Documentation, https://chain.kxco.ai/wallet/dev-docs
9
+ Project-URL: Repository, https://github.com/JackKXCO/kxco-post-quantum-verifiers
10
+ Keywords: post-quantum,ml-dsa,dilithium,webhook,fips-204,kxco
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Provides-Extra: oqs
17
+ Requires-Dist: liboqs-python>=0.10; extra == "oqs"
18
+ Provides-Extra: pqcrypto
19
+ Requires-Dist: pqcrypto>=0.3; extra == "pqcrypto"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+
23
+ # kxco-verify (Python)
24
+
25
+ Receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65 webhook signature scheme. Wire-format compatible with `@kxco/post-quantum` (npm), the Go verifier, and the Rust verifier.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install kxco-verify # core (HMAC, envelope, fingerprint)
31
+ pip install kxco-verify[oqs] # adds liboqs-python for ML-DSA
32
+ pip install kxco-verify[pqcrypto] # adds pqcrypto for ML-DSA
33
+ ```
34
+
35
+ The HMAC, envelope, fingerprint, and timestamp paths use only the Python standard library. ML-DSA-65 verification is lazy-loaded: it only requires a PQC backend when `verify_pq` is actually called.
36
+
37
+ ## Quick start (FastAPI)
38
+
39
+ ```python
40
+ from fastapi import FastAPI, Request, HTTPException
41
+ import os
42
+ import kxco_verify as kx
43
+
44
+ PINNED_KID = "aa29f37ab7f4b2cf" # current KXCO production kid (refresh from /.well-known if rotated)
45
+ PINNED_PUBKEY = bytes.fromhex("...3904 hex chars...")
46
+ HMAC_SECRET = os.environ["KXCO_WEBHOOK_SECRET"].encode()
47
+
48
+ app = FastAPI()
49
+
50
+ @app.post("/webhooks/kxco")
51
+ async def webhook(request: Request):
52
+ raw_body = await request.body()
53
+ headers = {k.lower(): v for k, v in request.headers.items()}
54
+
55
+ result = kx.verify_delivery(
56
+ headers=headers,
57
+ raw_body=raw_body,
58
+ hmac_secret=HMAC_SECRET,
59
+ pq_public_key=PINNED_PUBKEY,
60
+ pinned_kid=PINNED_KID,
61
+ )
62
+ if not result.ok:
63
+ raise HTTPException(401, "invalid signature")
64
+
65
+ return {"ok": True}
66
+ ```
67
+
68
+ ## Running tests
69
+
70
+ ```bash
71
+ cd python
72
+ python test_kxco_verify.py
73
+ ```
74
+
75
+ Expected: `All 6 vector tests passed.`
76
+
77
+ This verifies that the Python implementation produces identical outputs to `vectors.json` — the same file used by the JavaScript, Go, and Rust verifiers.
78
+
79
+ ## License
80
+
81
+ MIT.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ kxco_verify.py
3
+ pyproject.toml
4
+ kxco_verify.egg-info/PKG-INFO
5
+ kxco_verify.egg-info/SOURCES.txt
6
+ kxco_verify.egg-info/dependency_links.txt
7
+ kxco_verify.egg-info/requires.txt
8
+ kxco_verify.egg-info/top_level.txt
@@ -0,0 +1,9 @@
1
+
2
+ [dev]
3
+ pytest>=7
4
+
5
+ [oqs]
6
+ liboqs-python>=0.10
7
+
8
+ [pqcrypto]
9
+ pqcrypto>=0.3
@@ -0,0 +1 @@
1
+ kxco_verify
@@ -0,0 +1,208 @@
1
+ """kxco_verify — receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65
2
+ webhook signature scheme.
3
+
4
+ Wire-format compatible with @kxco/post-quantum (npm), the Go verifier, and the
5
+ Rust verifier.
6
+
7
+ The HMAC, envelope, fingerprint, and timestamp paths depend only on the Python
8
+ standard library.
9
+
10
+ ML-DSA-65 verification requires one of:
11
+ - `oqs` (Open Quantum Safe Python bindings; pip install oqs)
12
+ - `pqcrypto` (pip install pqcrypto, with ML-DSA backend)
13
+ The verifier auto-detects the available backend at import time.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import hmac
19
+ import time
20
+ from dataclasses import dataclass
21
+ from typing import Mapping, Optional
22
+
23
+ DEFAULT_REPLAY_WINDOW = 300 # seconds
24
+
25
+ # ── ML-DSA backend detection ────────────────────────────────────────────────
26
+ #
27
+ # Backend selection is LAZY: probing happens only on the first verify_pq call.
28
+ # This keeps the HMAC and envelope paths fully functional even if no PQC
29
+ # library is installed locally (the receiver may only verify HMAC and treat PQ
30
+ # as defence-in-depth that they'll wire up later).
31
+
32
+ _ml_dsa_backend: Optional[str] = None
33
+ _pqcrypto_ml_dsa = None
34
+ _backend_probed = False
35
+
36
+
37
+ def _probe_ml_dsa_backend() -> None:
38
+ """Lazy backend probe. Called from verify_pq only."""
39
+ global _ml_dsa_backend, _pqcrypto_ml_dsa, _backend_probed
40
+ if _backend_probed:
41
+ return
42
+ _backend_probed = True
43
+
44
+ try:
45
+ import oqs # type: ignore
46
+ probe = oqs.Signature("ML-DSA-65") # raises if liboqs shared lib missing
47
+ del probe
48
+ _ml_dsa_backend = "oqs"
49
+ return
50
+ except Exception:
51
+ pass
52
+
53
+ try:
54
+ from pqcrypto.sign import ml_dsa_65 as _ml_dsa_mod # type: ignore
55
+ _pqcrypto_ml_dsa = _ml_dsa_mod
56
+ _ml_dsa_backend = "pqcrypto"
57
+ except Exception:
58
+ _pqcrypto_ml_dsa = None
59
+
60
+
61
+ # ── Envelope + HMAC primitives (pure stdlib) ─────────────────────────────────
62
+
63
+ def envelope(timestamp: str, raw_body: bytes) -> bytes:
64
+ """Canonical signed envelope: timestamp + "." + raw_body bytes."""
65
+ if isinstance(raw_body, str):
66
+ raw_body = raw_body.encode("utf-8")
67
+ return timestamp.encode("utf-8") + b"." + raw_body
68
+
69
+
70
+ def hmac_hex(secret: bytes, timestamp: str, raw_body: bytes) -> str:
71
+ """HMAC-SHA-256 of the envelope, hex-encoded. No `sha256=` prefix."""
72
+ if isinstance(secret, str):
73
+ secret = secret.encode("utf-8")
74
+ mac = hmac.new(secret, envelope(timestamp, raw_body), hashlib.sha256)
75
+ return mac.hexdigest()
76
+
77
+
78
+ def verify_hmac(secret: bytes, timestamp: str, raw_body: bytes, sig_header: str) -> bool:
79
+ """Constant-time verify of the X-KXCO-Signature header.
80
+ Accepts the value with or without the `sha256=` prefix.
81
+ """
82
+ expected = "sha256=" + hmac_hex(secret, timestamp, raw_body)
83
+ given = sig_header if sig_header.startswith("sha256=") else "sha256=" + sig_header
84
+ return hmac.compare_digest(expected, given)
85
+
86
+
87
+ # ── ML-DSA-65 verify ─────────────────────────────────────────────────────────
88
+
89
+ def verify_pq(public_key: bytes, timestamp: str, raw_body: bytes, sig_header: str) -> bool:
90
+ """Verify the X-KXCO-PQ-Signature ML-DSA-65 signature.
91
+
92
+ Accepts header value with or without the `ml-dsa-65=` prefix.
93
+ Returns False on any error (invalid hex, invalid key, signature mismatch).
94
+ """
95
+ hex_sig = sig_header[len("ml-dsa-65="):] if sig_header.startswith("ml-dsa-65=") else sig_header
96
+ try:
97
+ sig_bytes = bytes.fromhex(hex_sig)
98
+ except ValueError:
99
+ return False
100
+
101
+ env = envelope(timestamp, raw_body)
102
+ _probe_ml_dsa_backend()
103
+
104
+ if _ml_dsa_backend == "oqs":
105
+ try:
106
+ import oqs # type: ignore
107
+ verifier = oqs.Signature("ML-DSA-65")
108
+ return verifier.verify(env, sig_bytes, public_key)
109
+ except Exception:
110
+ return False
111
+
112
+ if _ml_dsa_backend == "pqcrypto":
113
+ try:
114
+ _pqcrypto_ml_dsa.verify(public_key, sig_bytes + env)
115
+ return True
116
+ except Exception:
117
+ return False
118
+
119
+ raise RuntimeError(
120
+ "No ML-DSA-65 backend available. Install one of:\n"
121
+ " pip install liboqs-python # Open Quantum Safe (with liboqs built locally)\n"
122
+ " pip install pqcrypto # pqcrypto with ML-DSA backend"
123
+ )
124
+
125
+
126
+ # ── Kid / fingerprint ────────────────────────────────────────────────────────
127
+
128
+ def fingerprint(public_key: bytes) -> str:
129
+ """16-hex kid: first 8 bytes of SHA-256(public_key)."""
130
+ if isinstance(public_key, str):
131
+ public_key = bytes.fromhex(public_key)
132
+ return hashlib.sha256(public_key).hexdigest()[:16]
133
+
134
+
135
+ def kid_equals(a: str, b: str) -> bool:
136
+ """Constant-time string compare for kids."""
137
+ if len(a) != len(b):
138
+ return False
139
+ return hmac.compare_digest(a, b)
140
+
141
+
142
+ # ── Full delivery verifier ───────────────────────────────────────────────────
143
+
144
+ @dataclass
145
+ class VerifyResult:
146
+ hmac_ok: bool
147
+ pq_ok: bool
148
+ timestamp_ok: bool
149
+ kid_ok: bool
150
+
151
+ @property
152
+ def ok(self) -> bool:
153
+ return (self.hmac_ok or self.pq_ok) and self.timestamp_ok and self.kid_ok
154
+
155
+
156
+ def verify_delivery(
157
+ *,
158
+ headers: Mapping[str, str],
159
+ raw_body: bytes,
160
+ hmac_secret: Optional[bytes] = None,
161
+ pq_public_key: Optional[bytes] = None,
162
+ pinned_kid: Optional[str] = None,
163
+ window_seconds: int = DEFAULT_REPLAY_WINDOW,
164
+ now_unix: Optional[int] = None,
165
+ ) -> VerifyResult:
166
+ """Verify a KXCO webhook delivery.
167
+
168
+ `headers` should have lowercase keys. `raw_body` must be the exact bytes
169
+ received over the wire (do not re-stringify a parsed JSON object).
170
+
171
+ Either signature alone is sufficient; verifying both is defence-in-depth.
172
+ """
173
+ timestamp = headers.get("x-kxco-timestamp", "")
174
+ sig_hmac = headers.get("x-kxco-signature", "")
175
+ sig_pq = headers.get("x-kxco-pq-signature", "")
176
+ kid = headers.get("x-kxco-pq-kid", "")
177
+
178
+ try:
179
+ ts = int(timestamp)
180
+ except ValueError:
181
+ return VerifyResult(False, False, False, False)
182
+
183
+ now = now_unix if now_unix is not None else int(time.time())
184
+ timestamp_ok = abs(now - ts) <= window_seconds
185
+ kid_ok = (pinned_kid is None) or kid_equals(kid, pinned_kid)
186
+
187
+ hmac_ok = False
188
+ if hmac_secret is not None and sig_hmac and timestamp_ok:
189
+ hmac_ok = verify_hmac(hmac_secret, timestamp, raw_body, sig_hmac)
190
+
191
+ pq_ok = False
192
+ if pq_public_key is not None and sig_pq and timestamp_ok and kid_ok:
193
+ pq_ok = verify_pq(pq_public_key, timestamp, raw_body, sig_pq)
194
+
195
+ return VerifyResult(hmac_ok=hmac_ok, pq_ok=pq_ok, timestamp_ok=timestamp_ok, kid_ok=kid_ok)
196
+
197
+
198
+ __all__ = [
199
+ "envelope",
200
+ "hmac_hex",
201
+ "verify_hmac",
202
+ "verify_pq",
203
+ "fingerprint",
204
+ "kid_equals",
205
+ "verify_delivery",
206
+ "VerifyResult",
207
+ "DEFAULT_REPLAY_WINDOW",
208
+ ]
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "kxco-verify"
3
+ version = "1.0.0"
4
+ description = "Receiver-side verifier for the KXCO hybrid HMAC + ML-DSA-65 webhook signature scheme"
5
+ authors = [{ name = "KXCO by Knightsbridge", email = "hello@kxco.ai" }]
6
+ license = { text = "MIT" }
7
+ readme = "README.md"
8
+ requires-python = ">=3.9"
9
+ keywords = ["post-quantum", "ml-dsa", "dilithium", "webhook", "fips-204", "kxco"]
10
+ classifiers = [
11
+ "License :: OSI Approved :: MIT License",
12
+ "Programming Language :: Python :: 3",
13
+ "Topic :: Security :: Cryptography",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ oqs = ["liboqs-python>=0.10"]
18
+ pqcrypto = ["pqcrypto>=0.3"]
19
+ dev = ["pytest>=7"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://kxco.ai"
23
+ Documentation = "https://chain.kxco.ai/wallet/dev-docs"
24
+ Repository = "https://github.com/JackKXCO/kxco-post-quantum-verifiers"
25
+
26
+ [tool.setuptools]
27
+ # Single-module flat layout. Excludes test_kxco_verify.py from the wheel.
28
+ py-modules = ["kxco_verify"]
29
+
30
+ [build-system]
31
+ requires = ["setuptools>=61"]
32
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+