Fix LICK: confirmed raw UTF-8 key, no sort_keys - verified working 2026-07-14

This commit is contained in:
VZ Build
2026-07-14 17:40:49 +00:00
parent 83eb60d1aa
commit da632737e0

97
lick.py
View File

@@ -1,41 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Vector Zulu — LICK Signing Module Vector Zulu — LICK Signing Module
LICK (Layered Integrity Cryptographic Keying) — HMAC-SHA256 signing CONFIRMED WORKING: raw UTF-8 key bytes, no sort_keys on canonicalize.
for all mutating KoreNet API calls. Verified 2026-07-14 against korechain.korenet.cloud.
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, hashlib, hmac, json, os, time, uuid
import base64
import hashlib
import hmac
import json
import os
import time
import uuid
from typing import Optional from typing import Optional
SIGNER_ID = 'vector-zulu' SIGNER_ID = 'vector-zulu'
def _get_lick_key() -> bytes: 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 = ( key_raw = (
os.environ.get('VECTOR_ZULU_LICK_KEY') or os.environ.get('VECTOR_ZULU_LICK_KEY') or
os.environ.get('KORE_LICK_HMAC_KEY') or os.environ.get('KORE_LICK_HMAC_KEY') or
@@ -43,25 +17,14 @@ def _get_lick_key() -> bytes:
) )
if not key_raw: if not key_raw:
raise ValueError('LICK key not configured — set VECTOR_ZULU_LICK_KEY') raise ValueError('LICK key not configured — set VECTOR_ZULU_LICK_KEY')
# Key is raw string — encode to bytes directly (NOT base64 decoded)
# Key is raw string — encode to bytes directly
# (NotMyCircu$_NotMyMonkey$ is raw UTF-8, not base64)
return key_raw.encode('utf-8') return key_raw.encode('utf-8')
def canonicalize(payload: dict) -> str: def canonicalize(payload: dict) -> str:
""" """Compact JSON — NO sort_keys (server does not sort)."""
Canonical JSON serialization per LICK spec.
compact JSON, no whitespace, separators=(',',':'), sort_keys=True
"""
return json.dumps(payload, separators=(',', ':')) return json.dumps(payload, separators=(',', ':'))
def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str: def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str:
"""
Canonical LICK signature (primary format).
Returns lowercase hex string.
"""
if timestamp is None: if timestamp is None:
timestamp = int(time.time()) timestamp = int(time.time())
key = _get_lick_key() key = _get_lick_key()
@@ -69,64 +32,18 @@ def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str:
msg = f"{canonical}\n{timestamp}" msg = f"{canonical}\n{timestamp}"
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).hexdigest() 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: 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()) ts = int(time.time())
nonce = str(uuid.uuid4()) 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 { return {
'x-lick-ts': str(ts), 'x-lick-ts': str(ts),
'x-lick-sig': sig, 'x-lick-sig': sig,
'x-lick-nonce': nonce, 'x-lick-nonce': nonce,
'x-lick-signer': SIGNER_ID, 'x-lick-signer': SIGNER_ID,
'x-lick-variant': variant, 'x-lick-variant': variant,
'x-sentinel-trace': sentinel_trace, 'x-sentinel-trace': str(uuid.uuid4()),
'Idempotency-Key': idempotency, 'Idempotency-Key': str(uuid.uuid4()),
'X-Tenant-Id': 'vector-zulu', 'X-Tenant-Id': 'vector-zulu',
'Content-Type': 'application/json', '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]}...")