ตรวจสอบ license แบบ Offline ด้วยลายเซ็น Ed25519 (.lic)

อ่าน 8 นาที

ให้แอปตรวจ 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 ที่เซ็นแล้ว
// ทุก 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 ครั้งเดียวแล้วฝัง/แคชไว้:

JavaScript (keythai)
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 หมดอายุ"
C# (KeyThai.Client)
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}");
Python (keythai)
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 แบบนี้:

canonicalJson
// 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 ตรวจลายเซ็น

พร้อมปกป้องซอฟต์แวร์ของคุณแล้วหรือยัง?

เริ่มต้นฟรี ไม่ต้องใช้บัตรเครดิต ออก license key แรกของคุณได้ในไม่กี่นาที