From c344352279fe291ed627b1963abe0de24e81f71a Mon Sep 17 00:00:00 2001 From: VZ Build Date: Thu, 16 Jul 2026 05:57:35 +0000 Subject: [PATCH] Fix LICK: sign identical bytes sent on wire, use data=body not json=payload --- lick.py | 108 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 24 deletions(-) diff --git a/lick.py b/lick.py index b1757c7..379516b 100644 --- a/lick.py +++ b/lick.py @@ -1,15 +1,36 @@ #!/usr/bin/env python3 """ Vector Zulu — LICK Signing Module -CONFIRMED WORKING: raw UTF-8 key bytes, no sort_keys on canonicalize. -Verified 2026-07-14 against korechain.korenet.cloud. +CONFIRMED CONTRACT (per Corné Kotze handover 2026-07-15): + + body = compact JSON bytes sent on the wire (serialize ONCE) + signature = hex(HMAC-SHA256(key, body + "\\n" + unix_timestamp)) + +CRITICAL: sign and send IDENTICAL bytes. +Use data=body_bytes (not json=payload) in requests.post(). +Do NOT serialize twice. + +Required headers: + Authorization: Bearer + X-Tenant-Id: vector-zulu + x-lick-ts: + x-lick-sig: + x-lick-signer: vector-zulu + x-lick-variant: canonical + Idempotency-Key: + +Key: VECTOR-ZULU-LICK-KEY from Key Vault (loaded via env var VECTOR_ZULU_LICK_KEY) +Global API LICK keys are NOT accepted for x-lick-signer: vector-zulu. """ -import base64, hashlib, hmac, json, os, time, uuid -from typing import Optional + +import hashlib, hmac, json, os, time, uuid +from typing import Optional, Tuple SIGNER_ID = 'vector-zulu' + def _get_lick_key() -> bytes: + """Load LICK key from environment — raw UTF-8, not base64.""" key_raw = ( os.environ.get('VECTOR_ZULU_LICK_KEY') or os.environ.get('KORE_LICK_HMAC_KEY') or @@ -17,33 +38,72 @@ def _get_lick_key() -> bytes: ) if not key_raw: raise ValueError('LICK key not configured — set VECTOR_ZULU_LICK_KEY') - # Key is raw string — encode to bytes directly (NOT base64 decoded) return key_raw.encode('utf-8') -def canonicalize(payload: dict) -> str: - """Compact JSON — NO sort_keys (server does not sort).""" - return json.dumps(payload, separators=(',', ':')) -def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str: - if timestamp is None: - timestamp = int(time.time()) - key = _get_lick_key() - canonical = canonicalize(payload) - msg = f"{canonical}\n{timestamp}" - return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).hexdigest() +def prepare_request(payload: dict, idempotency_key: Optional[str] = None) -> Tuple[bytes, dict]: + """ + Serialize payload to bytes ONCE, sign them, return (body_bytes, headers). -def build_headers(payload: dict, variant: str = 'canonical') -> dict: - ts = int(time.time()) - nonce = str(uuid.uuid4()) - sig = sign_canonical(payload, ts) - return { + USAGE: + body_bytes, headers = prepare_request(payload) + resp = requests.post(url, data=body_bytes, headers=headers) + + Do NOT pass json=payload to requests — that would re-serialize + and produce different bytes from what was signed. + """ + # Serialize ONCE — compact, no whitespace + body_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8') + + ts = int(time.time()) + nonce = str(uuid.uuid4()) + idem_key = idempotency_key or str(uuid.uuid4()) + key = _get_lick_key() + + # Sign the exact bytes that will be sent on the wire + msg = body_bytes + b'\n' + str(ts).encode('utf-8') + sig = hmac.new(key, msg, hashlib.sha256).hexdigest() + + headers = { + 'Content-Type': 'application/json', + 'X-Tenant-Id': SIGNER_ID, 'x-lick-ts': str(ts), 'x-lick-sig': sig, 'x-lick-nonce': nonce, 'x-lick-signer': SIGNER_ID, - 'x-lick-variant': variant, + 'x-lick-variant': 'canonical', + 'Idempotency-Key': idem_key, 'x-sentinel-trace': str(uuid.uuid4()), - 'Idempotency-Key': str(uuid.uuid4()), - 'X-Tenant-Id': 'vector-zulu', - 'Content-Type': 'application/json', } + + return body_bytes, headers + + +def build_headers(payload: dict, variant: str = 'canonical') -> dict: + """ + Legacy helper — returns headers only. + Caller must use data=json.dumps(payload, separators=(',',':')).encode() + NOT json=payload. + """ + _, headers = prepare_request(payload) + return headers + + +def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str: + """Return hex LICK signature for a payload.""" + key = _get_lick_key() + body = json.dumps(payload, separators=(',', ':')).encode('utf-8') + ts = timestamp or int(time.time()) + msg = body + b'\n' + str(ts).encode('utf-8') + return hmac.new(key, msg, hashlib.sha256).hexdigest() + + +if __name__ == '__main__': + import os + os.environ['VECTOR_ZULU_LICK_KEY'] = "NotMyCircu$_NotMyMonkey$" + payload = {'test': 'lick', 'amount': 1000} + body_bytes, headers = prepare_request(payload) + print("Body bytes:", body_bytes) + print("Headers:") + for k, v in headers.items(): + print(f" {k}: {v}")