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.
- kxco_verify-1.0.0/PKG-INFO +81 -0
- kxco_verify-1.0.0/README.md +59 -0
- kxco_verify-1.0.0/kxco_verify.egg-info/PKG-INFO +81 -0
- kxco_verify-1.0.0/kxco_verify.egg-info/SOURCES.txt +8 -0
- kxco_verify-1.0.0/kxco_verify.egg-info/dependency_links.txt +1 -0
- kxco_verify-1.0.0/kxco_verify.egg-info/requires.txt +9 -0
- kxco_verify-1.0.0/kxco_verify.egg-info/top_level.txt +1 -0
- kxco_verify-1.0.0/kxco_verify.py +208 -0
- kxco_verify-1.0.0/pyproject.toml +32 -0
- kxco_verify-1.0.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"
|