Spaces:
Running
Running
fix(build): restore killinchu_szl_pqc_sign.py referenced by Dockerfile (unblocks build)
Browse filesThe Dockerfile COPYs killinchu_szl_pqc_sign.py and serve.py imports it, but it was absent from the Space repo causing BUILD_ERROR (checksum/not found). Restoring the existing tracked file. ADDITIVE; does not alter v1/v2/v3 logic.
Signed-off-by: Yachay <yachay@szlholdings.dev>
Co-authored-by: Perplexity Computer Agent <agent@perplexity.ai>
- killinchu_szl_pqc_sign.py +212 -0
killinchu_szl_pqc_sign.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# © 2026 Lutar, Stephen P. — SZL Holdings
|
| 3 |
+
# ORCID: 0009-0001-0110-4173
|
| 4 |
+
# Doctrine v11 — 749 declarations · 163 sorries · 14 unique axioms
|
| 5 |
+
"""
|
| 6 |
+
Killinchu — additive post-quantum / hybrid DSSE signing endpoints.
|
| 7 |
+
|
| 8 |
+
Registers, ADDITIVELY (a11oy-style `register(app, ns)`):
|
| 9 |
+
POST /khipu/sign?mode={ecdsa,pqc,hybrid}
|
| 10 |
+
POST /api/killinchu/v1/khipu/sign?mode={ecdsa,pqc,hybrid}
|
| 11 |
+
|
| 12 |
+
Honest framing
|
| 13 |
+
--------------
|
| 14 |
+
* ECDSA P-256 + SHA-256 is the DEFAULT (`mode=ecdsa`). PQC is ADDITIVE.
|
| 15 |
+
* `mode=pqc` → ML-DSA-65 (NIST FIPS 204) only.
|
| 16 |
+
* `mode=hybrid` → BOTH ECDSA P-256 and ML-DSA-65; verify requires both.
|
| 17 |
+
Defense procurement (killinchu vertical) asks about PQC; hybrid mode live =
|
| 18 |
+
real competitive advantage.
|
| 19 |
+
|
| 20 |
+
ML-DSA backend resolution (graceful): liboqs via `oqs-python` (prod) →
|
| 21 |
+
pure-Python `dilithium-py` → if neither present, `mode=ecdsa` still works and
|
| 22 |
+
pqc/hybrid return HTTP 503 with an honest message (never a fake signature).
|
| 23 |
+
|
| 24 |
+
Sign: Yachay <yachay@szlholdings.dev>. Perplexity Computer Agent.
|
| 25 |
+
"""
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import base64
|
| 29 |
+
import hashlib
|
| 30 |
+
from typing import Optional
|
| 31 |
+
|
| 32 |
+
from cryptography.hazmat.primitives import hashes, serialization
|
| 33 |
+
from cryptography.hazmat.primitives.asymmetric import ec
|
| 34 |
+
from cryptography.exceptions import InvalidSignature
|
| 35 |
+
|
| 36 |
+
ECDSA_TYPE = "ECDSA-P256-SHA256"
|
| 37 |
+
MLDSA_TYPE = "ML-DSA-65"
|
| 38 |
+
|
| 39 |
+
_MLDSA_BACKEND: Optional[str] = None
|
| 40 |
+
_PROC_SIGNERS: dict = {}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _detect_mldsa() -> Optional[str]:
|
| 44 |
+
global _MLDSA_BACKEND
|
| 45 |
+
if _MLDSA_BACKEND is not None:
|
| 46 |
+
return _MLDSA_BACKEND if _MLDSA_BACKEND != "none" else None
|
| 47 |
+
try:
|
| 48 |
+
import oqs # type: ignore
|
| 49 |
+
if hasattr(oqs, "Signature") and hasattr(oqs, "get_enabled_sig_mechanisms"):
|
| 50 |
+
_MLDSA_BACKEND = "oqs"
|
| 51 |
+
return "oqs"
|
| 52 |
+
except Exception:
|
| 53 |
+
pass
|
| 54 |
+
try:
|
| 55 |
+
from dilithium_py.ml_dsa import ML_DSA_65 # noqa: F401
|
| 56 |
+
_MLDSA_BACKEND = "dilithium_py"
|
| 57 |
+
return "dilithium_py"
|
| 58 |
+
except Exception:
|
| 59 |
+
pass
|
| 60 |
+
_MLDSA_BACKEND = "none"
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _mldsa_keypair():
|
| 65 |
+
b = _detect_mldsa()
|
| 66 |
+
if b == "oqs":
|
| 67 |
+
import oqs # type: ignore
|
| 68 |
+
s = oqs.Signature(MLDSA_TYPE)
|
| 69 |
+
pk = s.generate_keypair()
|
| 70 |
+
return pk, s.export_secret_key()
|
| 71 |
+
if b == "dilithium_py":
|
| 72 |
+
from dilithium_py.ml_dsa import ML_DSA_65
|
| 73 |
+
return ML_DSA_65.keygen()
|
| 74 |
+
raise RuntimeError("no ML-DSA backend")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _mldsa_sign(sk, msg):
|
| 78 |
+
b = _detect_mldsa()
|
| 79 |
+
if b == "oqs":
|
| 80 |
+
import oqs # type: ignore
|
| 81 |
+
with oqs.Signature(MLDSA_TYPE, sk) as s:
|
| 82 |
+
return s.sign(msg)
|
| 83 |
+
if b == "dilithium_py":
|
| 84 |
+
from dilithium_py.ml_dsa import ML_DSA_65
|
| 85 |
+
return ML_DSA_65.sign(sk, msg)
|
| 86 |
+
raise RuntimeError("no ML-DSA backend")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _mldsa_verify(pk, msg, sig):
|
| 90 |
+
b = _detect_mldsa()
|
| 91 |
+
if b == "oqs":
|
| 92 |
+
import oqs # type: ignore
|
| 93 |
+
with oqs.Signature(MLDSA_TYPE) as s:
|
| 94 |
+
return bool(s.verify(msg, sig, pk))
|
| 95 |
+
if b == "dilithium_py":
|
| 96 |
+
from dilithium_py.ml_dsa import ML_DSA_65
|
| 97 |
+
return bool(ML_DSA_65.verify(pk, msg, sig))
|
| 98 |
+
raise RuntimeError("no ML-DSA backend")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _pae(payload_type: str, payload: bytes) -> bytes:
|
| 102 |
+
t = payload_type.encode()
|
| 103 |
+
return b"DSSEv1 %d %s %d %s" % (len(t), t, len(payload), payload)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _b64(d: bytes) -> str:
|
| 107 |
+
return base64.standard_b64encode(d).decode()
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _keyid(b: bytes) -> str:
|
| 111 |
+
return hashlib.sha256(b).hexdigest()[:16]
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _proc_signer(mode: str):
|
| 115 |
+
"""Process-local signer per mode (demo/runtime; resets on restart — honest)."""
|
| 116 |
+
if mode in _PROC_SIGNERS:
|
| 117 |
+
return _PROC_SIGNERS[mode]
|
| 118 |
+
bundle = {"ecdsa": None, "mldsa_pk": None, "mldsa_sk": None}
|
| 119 |
+
if mode in ("ecdsa", "hybrid"):
|
| 120 |
+
bundle["ecdsa"] = ec.generate_private_key(ec.SECP256R1())
|
| 121 |
+
if mode in ("pqc", "hybrid"):
|
| 122 |
+
bundle["mldsa_pk"], bundle["mldsa_sk"] = _mldsa_keypair()
|
| 123 |
+
_PROC_SIGNERS[mode] = bundle
|
| 124 |
+
return bundle
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
PAYLOAD_TYPE = "application/vnd.szl.khipu+json"
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _sign(payload: bytes, mode: str) -> dict:
|
| 131 |
+
data = _pae(PAYLOAD_TYPE, payload)
|
| 132 |
+
s = _proc_signer(mode)
|
| 133 |
+
sigs = []
|
| 134 |
+
ecdsa_ok = mldsa_ok = None
|
| 135 |
+
if mode in ("ecdsa", "hybrid"):
|
| 136 |
+
sig = s["ecdsa"].sign(data, ec.ECDSA(hashes.SHA256()))
|
| 137 |
+
raw = s["ecdsa"].public_key().public_bytes(
|
| 138 |
+
serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)
|
| 139 |
+
sigs.append({"keyid": _keyid(raw), "sig": _b64(sig), "sig_type": ECDSA_TYPE})
|
| 140 |
+
try:
|
| 141 |
+
s["ecdsa"].public_key().verify(sig, data, ec.ECDSA(hashes.SHA256()))
|
| 142 |
+
ecdsa_ok = True
|
| 143 |
+
except InvalidSignature:
|
| 144 |
+
ecdsa_ok = False
|
| 145 |
+
if mode in ("pqc", "hybrid"):
|
| 146 |
+
sig = _mldsa_sign(s["mldsa_sk"], data)
|
| 147 |
+
sigs.append({"keyid": _keyid(s["mldsa_pk"]), "sig": _b64(sig), "sig_type": MLDSA_TYPE})
|
| 148 |
+
mldsa_ok = _mldsa_verify(s["mldsa_pk"], data, sig)
|
| 149 |
+
if mode == "ecdsa":
|
| 150 |
+
verified = bool(ecdsa_ok)
|
| 151 |
+
elif mode == "pqc":
|
| 152 |
+
verified = bool(mldsa_ok)
|
| 153 |
+
else:
|
| 154 |
+
verified = bool(ecdsa_ok) and bool(mldsa_ok)
|
| 155 |
+
return {
|
| 156 |
+
"mode": mode,
|
| 157 |
+
"sig_types": [x["sig_type"] for x in sigs],
|
| 158 |
+
"verified": verified,
|
| 159 |
+
"doctrine": "v11",
|
| 160 |
+
"envelope": {
|
| 161 |
+
"payload": _b64(payload),
|
| 162 |
+
"payloadType": PAYLOAD_TYPE,
|
| 163 |
+
"signatures": sigs,
|
| 164 |
+
},
|
| 165 |
+
"disclosure": (
|
| 166 |
+
"ECDSA P-256 is the default; PQC (ML-DSA-65 / NIST FIPS 204) is "
|
| 167 |
+
"additive. Hybrid signs with both. Per-process keys reset on restart "
|
| 168 |
+
"(honest). No fake signatures: pqc/hybrid require a real ML-DSA backend."
|
| 169 |
+
),
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def register(app, ns: str = "killinchu") -> None:
|
| 174 |
+
from fastapi import Request
|
| 175 |
+
from fastapi.responses import JSONResponse
|
| 176 |
+
|
| 177 |
+
async def _handler(request: Request) -> JSONResponse:
|
| 178 |
+
mode = (request.query_params.get("mode") or "ecdsa").lower()
|
| 179 |
+
if mode not in ("ecdsa", "pqc", "hybrid"):
|
| 180 |
+
return JSONResponse({"error": f"unknown mode '{mode}'"}, status_code=400)
|
| 181 |
+
body = await request.body()
|
| 182 |
+
payload = body if body else b"{}"
|
| 183 |
+
if mode in ("pqc", "hybrid") and _detect_mldsa() is None:
|
| 184 |
+
return JSONResponse(
|
| 185 |
+
{"error": "ML-DSA backend unavailable; install 'oqs' or 'dilithium-py'. "
|
| 186 |
+
"ECDSA mode still available.", "mode": mode},
|
| 187 |
+
status_code=503,
|
| 188 |
+
)
|
| 189 |
+
try:
|
| 190 |
+
return JSONResponse(_sign(payload, mode))
|
| 191 |
+
except Exception as e: # never fake a signature
|
| 192 |
+
return JSONResponse({"error": str(e), "mode": mode}, status_code=503)
|
| 193 |
+
|
| 194 |
+
# Race-aware / additive: a sibling may already own POST /khipu/sign. We never
|
| 195 |
+
# clobber it. The namespaced PQC path is always registered; the bare path and
|
| 196 |
+
# an explicit /khipu/sign/pqc path are registered as available additive
|
| 197 |
+
# surfaces. Callers get genuine ML-DSA via the namespaced or /pqc routes
|
| 198 |
+
# regardless of who owns the bare path.
|
| 199 |
+
existing = set()
|
| 200 |
+
for r in getattr(app, "routes", []):
|
| 201 |
+
path = getattr(r, "path", None)
|
| 202 |
+
methods = getattr(r, "methods", None) or set()
|
| 203 |
+
if path and "POST" in methods:
|
| 204 |
+
existing.add(path)
|
| 205 |
+
|
| 206 |
+
# Always-additive PQC surfaces (guaranteed not to collide).
|
| 207 |
+
app.add_api_route(f"/api/{ns}/v1/khipu/sign", _handler, methods=["POST"])
|
| 208 |
+
app.add_api_route("/khipu/sign/pqc", _handler, methods=["POST"])
|
| 209 |
+
|
| 210 |
+
# Bare path only if free (don't clobber a sibling's existing endpoint).
|
| 211 |
+
if "/khipu/sign" not in existing:
|
| 212 |
+
app.add_api_route("/khipu/sign", _handler, methods=["POST"])
|