Fix LICK: confirmed raw UTF-8 key, no sort_keys - verified working 2026-07-14
This commit is contained in:
97
lick.py
97
lick.py
@@ -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]}...")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user