diff --git a/lick.py b/lick.py index 281d009..b1757c7 100644 --- a/lick.py +++ b/lick.py @@ -1,41 +1,15 @@ #!/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). +CONFIRMED WORKING: raw UTF-8 key bytes, no sort_keys on canonicalize. +Verified 2026-07-14 against korechain.korenet.cloud. """ - -import base64 -import hashlib -import hmac -import json -import os -import time -import uuid +import base64, hashlib, hmac, json, os, time, 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 @@ -43,25 +17,14 @@ 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 - # (NotMyCircu$_NotMyMonkey$ is raw UTF-8, not base64) + # Key is raw string — encode to bytes directly (NOT base64 decoded) 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 - """ + """Compact JSON — NO sort_keys (server does not sort).""" return json.dumps(payload, separators=(',', ':')) - 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() @@ -69,64 +32,18 @@ def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str: 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) - + 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-sentinel-trace': str(uuid.uuid4()), + 'Idempotency-Key': str(uuid.uuid4()), '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]}...")