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
|
||||
"""
|
||||
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)
|
||||
|
||||
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]}...")
|
||||
|
||||
Reference in New Issue
Block a user