#!/usr/bin/env python3 """ Vector Zulu — LICK Signing Module LICK (Layered Integrity Cryptographic Keying) — HMAC-SHA256 signing for all mutating KoreNet API calls. Canonical format (primary): signatureInput = canonicalJSON(payload) + "\n" + unixTimestamp signature = hex(HMAC-SHA256(base64Decode(lickKey), UTF8(signatureInput))) Universal format (legacy, replay protection): signatureInput = canonicalJSON(payload) + "|" + timestamp + "|" + nonce + "|" + signerId signature = base64(HMAC-SHA256(base64Decode(lickKey), UTF8(signatureInput))) Per welcome pack section 5: json.dumps with separators=(',',':') and sort_keys=True. Key is base64-encoded in Key Vault; decode to raw bytes before computing HMAC. Timestamp freshness: server rejects if |now - x-lick-ts| > 300s (5 minutes). """ import base64 import hashlib import hmac import json import os import time import uuid from typing import Optional SIGNER_ID = 'vector-zulu' def _get_lick_key() -> bytes: """ Load LICK HMAC key from environment. Key is stored as raw string (NotMyCircu$_NotMyMonkey$) — NOT base64 encoded. Per secrets file: VECTOR_ZULU_LICK_KEY='NotMyCircu$_NotMyMonkey$' """ 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') # Try base64 decode first (for keys that are base64-encoded in Key Vault) # Fall back to raw bytes if not valid base64 try: decoded = base64.b64decode(key_raw, validate=True) return decoded except Exception: # Raw string key — encode to bytes directly return key_raw.encode('utf-8') def canonicalize(payload: dict) -> str: """ Canonical JSON serialization per LICK spec. compact JSON, no whitespace, separators=(',',':'), sort_keys=True """ return json.dumps(payload, separators=(',', ':'), sort_keys=True) def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str: """ Canonical LICK signature (primary format). Returns lowercase hex string. """ 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 sign_universal(payload: dict, timestamp: Optional[int] = None, nonce: Optional[str] = None, signer_id: str = SIGNER_ID) -> str: """ Universal LICK signature (legacy format with replay protection). Returns base64-encoded string. """ if timestamp is None: timestamp = int(time.time()) if nonce is None: nonce = str(uuid.uuid4()) key = _get_lick_key() canonical = canonicalize(payload) msg = f"{canonical}|{timestamp}|{nonce}|{signer_id}" raw = hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() return base64.b64encode(raw).decode('ascii') def build_headers(payload: dict, variant: str = 'canonical') -> dict: """ Build the full set of LICK HTTP headers for a request. Returns dict of headers to merge into request. """ ts = int(time.time()) nonce = str(uuid.uuid4()) sentinel_trace = str(uuid.uuid4()) idempotency = str(uuid.uuid4()) if variant == 'universal': sig = sign_universal(payload, ts, nonce, SIGNER_ID) else: sig = sign_canonical(payload, ts) return { 'x-lick-ts': str(ts), 'x-lick-sig': sig, 'x-lick-nonce': nonce, 'x-lick-signer': SIGNER_ID, 'x-lick-variant': variant, 'x-sentinel-trace': sentinel_trace, 'Idempotency-Key': idempotency, 'X-Tenant-Id': 'vector-zulu', 'Content-Type': 'application/json', } if __name__ == '__main__': import os os.environ['VECTOR_ZULU_LICK_KEY'] = "NotMyCircu$_NotMyMonkey$" payload = {'test': 'payload', 'amount': 1000000} print("=== Canonical ===") headers = build_headers(payload, 'canonical') for k, v in headers.items(): print(f" {k}: {v[:40]}...") print("\n=== Universal ===") headers = build_headers(payload, 'universal') for k, v in headers.items(): print(f" {k}: {v[:40]}...")