ตรวจสอบ license แบบ Offline ด้วยลายเซ็น Ed25519 (.lic)
ให้แอปตรวจ license ได้แม้ไม่มีอินเทอร์เน็ต ด้วยลายเซ็นดิจิทัล Ed25519 และไฟล์ .lic พร้อมตัวอย่าง verify ด้วย SDK ทั้ง JS, C# และ Python
จะเกิดอะไรขึ้นถ้าผู้ใช้แอปของคุณไม่มีอินเทอร์เน็ต? หรือถ้ามีคนทำ proxy ปลอม response ว่า valid: true ให้ตลอด? คำตอบคือ ลายเซ็นดิจิทัล Ed25519 — ทุก response ของ KeyThai ถูกเซ็นด้วย private key ของระบบ และแอปคุณ verify ได้ด้วย public key ที่ฝังไว้ โดยไม่ต้องเชื่อใจเครือข่าย
หลักการ: เซ็น payload ด้วย Ed25519
ทุก response ของ activate/validate มี field signature ซึ่งเป็นลายเซ็น Ed25519 (base64) ที่เซ็นทับ canonical JSON ของ payload (ทุก field ยกเว้น signature เอง):
// ทุก response ของ activate/validate ถูกเซ็นด้วย Ed25519
{
"valid": true,
"status": "active",
"expires_at": 1767225600,
"machine_count": 1,
"max_activations": 3,
"policy": { "type": "subscription", "offline_allowed": true },
"issued_at": 1717286400,
"nonce": "8f3a...",
"signature": "base64-ed25519-signature..." // ← เซ็นทับ canonical JSON ของ field ที่เหลือ
}เนื่องจาก private key อยู่ที่ฝั่ง KeyThai เท่านั้น ไม่มีใครปลอมลายเซ็นที่ผ่านการ verify ได้ แม้จะรู้ public key ก็ตาม
Verify ด้วย SDK (JS / C# / Python)
SDK ทุกภาษามีฟังก์ชัน verify ในตัว เพียงดึง public key (JWK) จาก GET /v1/keys ครั้งเดียวแล้วฝัง/แคชไว้:
import { verifySignature, verifyLicFile } from "keythai";
// 1) ดึง public key ครั้งเดียวแล้วฝังในแอป (หรือ cache ไว้)
const { keys } = await client.listKeys();
const jwk = keys[0].publicJwk;
// 2) ตรวจ response สด
const res = await client.validate(key, { fingerprint });
if (!(await verifySignature(jwk, res))) {
throw new Error("ลายเซ็นไม่ถูกต้อง — response อาจถูกแก้ไข");
}
// 3) ตรวจไฟล์ .lic แบบ offline (เช็คทั้งลายเซ็น + วันหมดอายุ)
const lic = await verifyLicFile(jwk, licFileContents);
if (!lic.valid) throw new Error(lic.reason); // เช่น "license หมดอายุ"using KeyThai;
var keys = await client.ListKeysAsync();
var jwk = keys.Keys[0].PublicJwk;
LicFileResult lic = KeyThaiVerifier.VerifyLicFile(licFileContents, jwk);
if (!lic.Valid) throw new Exception(lic.Reason); // "license หมดอายุ"
Console.WriteLine($"ใช้ได้ถึง {lic.Payload!.ExpiresAt}");from keythai import verify_lic_file
jwk = client.list_keys()["keys"][0]["publicJwk"]
result = verify_lic_file(jwk, lic_file_contents)
if not result["valid"]:
raise RuntimeError(result["reason"]) # "license หมดอายุ"verifyLicFile ตรวจทั้งลายเซ็นและ expires_at ให้ในขั้นตอนเดียว — ถ้าหมดอายุจะคืน { valid: false, reason: "license หมดอายุ" }
ไฟล์ .lic สำหรับ offline เต็มรูปแบบ
สำหรับแอปที่ต้องทำงาน offline สนิท คุณ export license เป็นไฟล์ .lic ได้ ซึ่งคือ base64( JSON({ payload, signature, kid }) ) ผู้ใช้นำไฟล์ไปวางในแอป แล้วแอป verify ด้วย public key ที่ฝังไว้ — ไม่ต้องต่อเน็ตเลยแม้แต่ครั้งเดียว
เบื้องหลัง: canonical JSON ต้องตรงเป๊ะ
กุญแจสำคัญของการ verify คือทั้งสองฝั่งต้องสร้างสตริง JSON ที่ “เหมือนกันทุก byte” ก่อนเซ็น/ตรวจ ไม่งั้นลายเซ็นจะไม่ตรง KeyThai ใช้กติกา canonical JSON แบบนี้:
// canonical JSON: เรียง key ตามตัวอักษร, ตัด undefined, ไม่มี whitespace
// ฝั่ง server เซ็นทับสตริงนี้ ฝั่ง client ต้องสร้างสตริงเดียวกันเป๊ะ ๆ
function canonicalJson(value) {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
const entries = Object.entries(value)
.filter(([, v]) => v !== undefined)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
}
// SDK ของ KeyThai ทำส่วนนี้ให้แล้ว — เรียก verifySignature() ได้เลยโชคดีที่ SDK จัดการส่วนนี้ให้หมดแล้ว คุณแค่เรียก verifySignature หรือ verifyLicFile ก็พอ ถ้าอยากเขียนเองในภาษาอื่น ให้คัดลอก semantics นี้ไปให้ตรง (เรียง key, ตัด undefined, ไม่มี whitespace) แล้วใช้ WebCrypto/ไลบรารี Ed25519 ตรวจลายเซ็น