Fix LICK: sign identical bytes sent on wire, use data=body not json=payload
This commit is contained in:
108
lick.py
108
lick.py
@@ -1,15 +1,36 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Vector Zulu — LICK Signing Module
|
Vector Zulu — LICK Signing Module
|
||||||
CONFIRMED WORKING: raw UTF-8 key bytes, no sort_keys on canonicalize.
|
CONFIRMED CONTRACT (per Corné Kotze handover 2026-07-15):
|
||||||
Verified 2026-07-14 against korechain.korenet.cloud.
|
|
||||||
|
body = compact JSON bytes sent on the wire (serialize ONCE)
|
||||||
|
signature = hex(HMAC-SHA256(key, body + "\\n" + unix_timestamp))
|
||||||
|
|
||||||
|
CRITICAL: sign and send IDENTICAL bytes.
|
||||||
|
Use data=body_bytes (not json=payload) in requests.post().
|
||||||
|
Do NOT serialize twice.
|
||||||
|
|
||||||
|
Required headers:
|
||||||
|
Authorization: Bearer <Vector Zulu JWT>
|
||||||
|
X-Tenant-Id: vector-zulu
|
||||||
|
x-lick-ts: <unix seconds>
|
||||||
|
x-lick-sig: <hex signature>
|
||||||
|
x-lick-signer: vector-zulu
|
||||||
|
x-lick-variant: canonical
|
||||||
|
Idempotency-Key: <stable unique value>
|
||||||
|
|
||||||
|
Key: VECTOR-ZULU-LICK-KEY from Key Vault (loaded via env var VECTOR_ZULU_LICK_KEY)
|
||||||
|
Global API LICK keys are NOT accepted for x-lick-signer: vector-zulu.
|
||||||
"""
|
"""
|
||||||
import base64, hashlib, hmac, json, os, time, uuid
|
|
||||||
from typing import Optional
|
import hashlib, hmac, json, os, time, uuid
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
SIGNER_ID = 'vector-zulu'
|
SIGNER_ID = 'vector-zulu'
|
||||||
|
|
||||||
|
|
||||||
def _get_lick_key() -> bytes:
|
def _get_lick_key() -> bytes:
|
||||||
|
"""Load LICK key from environment — raw UTF-8, not base64."""
|
||||||
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
|
||||||
@@ -17,33 +38,72 @@ 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)
|
|
||||||
return key_raw.encode('utf-8')
|
return key_raw.encode('utf-8')
|
||||||
|
|
||||||
def canonicalize(payload: dict) -> str:
|
|
||||||
"""Compact JSON — NO sort_keys (server does not sort)."""
|
|
||||||
return json.dumps(payload, separators=(',', ':'))
|
|
||||||
|
|
||||||
def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str:
|
def prepare_request(payload: dict, idempotency_key: Optional[str] = None) -> Tuple[bytes, dict]:
|
||||||
if timestamp is None:
|
"""
|
||||||
timestamp = int(time.time())
|
Serialize payload to bytes ONCE, sign them, return (body_bytes, headers).
|
||||||
key = _get_lick_key()
|
|
||||||
canonical = canonicalize(payload)
|
|
||||||
msg = f"{canonical}\n{timestamp}"
|
|
||||||
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
||||||
|
|
||||||
def build_headers(payload: dict, variant: str = 'canonical') -> dict:
|
USAGE:
|
||||||
ts = int(time.time())
|
body_bytes, headers = prepare_request(payload)
|
||||||
nonce = str(uuid.uuid4())
|
resp = requests.post(url, data=body_bytes, headers=headers)
|
||||||
sig = sign_canonical(payload, ts)
|
|
||||||
return {
|
Do NOT pass json=payload to requests — that would re-serialize
|
||||||
|
and produce different bytes from what was signed.
|
||||||
|
"""
|
||||||
|
# Serialize ONCE — compact, no whitespace
|
||||||
|
body_bytes = json.dumps(payload, separators=(',', ':')).encode('utf-8')
|
||||||
|
|
||||||
|
ts = int(time.time())
|
||||||
|
nonce = str(uuid.uuid4())
|
||||||
|
idem_key = idempotency_key or str(uuid.uuid4())
|
||||||
|
key = _get_lick_key()
|
||||||
|
|
||||||
|
# Sign the exact bytes that will be sent on the wire
|
||||||
|
msg = body_bytes + b'\n' + str(ts).encode('utf-8')
|
||||||
|
sig = hmac.new(key, msg, hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Id': SIGNER_ID,
|
||||||
'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': 'canonical',
|
||||||
|
'Idempotency-Key': idem_key,
|
||||||
'x-sentinel-trace': str(uuid.uuid4()),
|
'x-sentinel-trace': str(uuid.uuid4()),
|
||||||
'Idempotency-Key': str(uuid.uuid4()),
|
|
||||||
'X-Tenant-Id': 'vector-zulu',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return body_bytes, headers
|
||||||
|
|
||||||
|
|
||||||
|
def build_headers(payload: dict, variant: str = 'canonical') -> dict:
|
||||||
|
"""
|
||||||
|
Legacy helper — returns headers only.
|
||||||
|
Caller must use data=json.dumps(payload, separators=(',',':')).encode()
|
||||||
|
NOT json=payload.
|
||||||
|
"""
|
||||||
|
_, headers = prepare_request(payload)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str:
|
||||||
|
"""Return hex LICK signature for a payload."""
|
||||||
|
key = _get_lick_key()
|
||||||
|
body = json.dumps(payload, separators=(',', ':')).encode('utf-8')
|
||||||
|
ts = timestamp or int(time.time())
|
||||||
|
msg = body + b'\n' + str(ts).encode('utf-8')
|
||||||
|
return hmac.new(key, msg, hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import os
|
||||||
|
os.environ['VECTOR_ZULU_LICK_KEY'] = "NotMyCircu$_NotMyMonkey$"
|
||||||
|
payload = {'test': 'lick', 'amount': 1000}
|
||||||
|
body_bytes, headers = prepare_request(payload)
|
||||||
|
print("Body bytes:", body_bytes)
|
||||||
|
print("Headers:")
|
||||||
|
for k, v in headers.items():
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
|||||||
Reference in New Issue
Block a user