#!/usr/bin/env python3 """ Vector Zulu — LICK Signing Module 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 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 os.environ.get('LICK_HMAC_SECRET', '') ) if not key_raw: raise ValueError('LICK key not configured — set VECTOR_ZULU_LICK_KEY') return key_raw.encode('utf-8') def prepare_request(payload: dict, idempotency_key: Optional[str] = None) -> Tuple[bytes, dict]: """ Serialize payload to bytes ONCE, sign them, return (body_bytes, headers). 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': 'canonical', 'Idempotency-Key': idem_key, 'x-sentinel-trace': str(uuid.uuid4()), } 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}")