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

99
lick.py
View File

@@ -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]}...")