Skip to content

ADR-0001. Chứng chỉ license ký Ed25519, xác minh offline qua Redis

TrườngGiá trị
StatusAccepted
Date2026-03-20
DecidersPhat Nguyen
Supersedes-

Bối cảnh

  • Mọi request trong mọi service (sale, commerce, inventory, …) đều cần biết trạng thái license, tier và tính năng của một merchant / user.
  • Một lệnh gọi HTTP đồng bộ tới licensing ở mỗi request sẽ ràng buộc mọi service vào độ sẵn sàng của licensing và làm tăng độ trễ trên đường đi nóng (hot path).
  • Trạng thái license thay đổi hiếm khi (issue / suspend / renew / revoke), nhưng được đọc liên tục - đặc tính đọc nhiều, ghi ít.
  • Dữ liệu phải chống giả mạo (consumer không được phép giả ra payload "PERPETUAL/all-features") và phải bảo mật (cấu hình tính năng không được đọc ra từ một bản dump Redis bị rò rỉ).

Quyết định

Chúng ta sẽ để licensing ký một chứng chỉ tự chứa cho mỗi license và cache nó trong Redis tại lic:certs:<entityType>:<entityId>. Consumer xác minh chứng chỉ offline qua LicenseMiddleware của @nx/core - chúng không bao giờ gọi licensing tại thời điểm chạy.

Lớp bọc của chứng chỉ là aes-256-gcm+ed25519:

  • ILicenseCertificatePayload (status, tier, features, giới hạn kích hoạt, thời điểm hết hạn) được mã hoá AES-256-GCM bằng APP_ENV_APPLICATION_SECRET.
  • Phần ciphertext được ký Ed25519 bằng APP_ENV_LICENSING_ED25519_PRIVATE_KEY; consumer xác minh bằng public key đóng gói sẵn trong @nx/core.
  • Payload mang certExpiresAt; key Redis cũng được đặt EX = APP_ENV_LICENSING_CERT_TTL_SECONDS (mặc định 86400).

Licensing publish lại sau mỗi thay đổi vòng đời (issue / suspend / reinstate / renew / revoke) và khi hết hạn theo cơ chế lazy. LicenseMiddleware hoạt động theo nguyên tắc fail-open: chứng chỉ thiếu / hết hạn / không xác minh được sẽ cho ra một license context null ("không xác định", chứ không phải "chưa có license").

Hệ quả

ƯuNhược
Không ràng buộc tại thời điểm chạy - consumer không gọi licensingMỗi lần publish license phải làm crypto bất đối xứng + AES (rẻ, nhưng vẫn có chi phí thật)
Đường đọc chỉ là một lệnh Redis GET + xác minh cục bộAPP_ENV_APPLICATION_SECRET dùng chung phải được phân phối tới mọi verifier (secret đối xứng lan ra nhiều nơi)
Chống giả mạo (Ed25519) + bảo mật (AES)Xoay vòng keypair đòi hỏi việc thay public key có phối hợp + publish lại
Trạng thái cũ bị giới hạn bởi TTL ngay cả khi bỏ lỡ một lần publishSửa tính năng qua CRUD không publish lại - chứng chỉ có thể trễ so với DB cho tới khi có một hành động vòng đời
Fail-open tránh việc licensing gặp sự cố kéo sập mọi serviceFail-open nghĩa là Redis gặp sự cố sẽ âm thầm làm suy giảm kiểm soát - các cổng kiểm tra phải xử lý null một cách có chủ ý

Phương án đã cân nhắc

Phương ánƯuNhượcVì sao loại
Gọi HTTP validate đồng bộ ở mỗi requestLuôn lấy dữ liệu mới nhấtRàng buộc mọi service vào độ sẵn sàng + độ trễ của licensingChi phí trên đường đi nóng & phạm vi ảnh hưởng không chấp nhận được
Dùng JWT (bất đối xứng) thay cho lớp bọc AES+Ed25519Chuẩn, nhiều công cụ hỗ trợPayload đọc được (base64) - cấu hình tính năng bị lộVi phạm yêu cầu bảo mật
Chứng chỉ chỉ dùng HMAC đối xứngĐơn giản hơn, một secret duy nhấtBất kỳ consumer nào giữ secret đều giả mạo được chứng chỉCần tách bạch tin cậy giữa issuer và verifier
Broadcast thay đổi license qua KafkaTheo mô hình đẩy, event-sourcedThêm phụ thuộc vào broker cho một nhu cầu vốn là đọc theo yêu cầu; consumer phải lưu trạng tháiQuá phức tạp cho hồ sơ đọc nhiều/ghi ít; cache Redis là đủ

Tham chiếu

  • licensing/src/services/licensing/licensing-base.service.ts - publishCertificate()
  • core/src/services/license/certs/signer.ts - LicenseCertSignerHelper.sign
  • core/src/services/license/certs/verifier.ts - LicenseCertVerifierHelper.verify
  • core/src/middlewares/license/license.middleware.ts - LicenseProvider (fail-open)
  • core/src/services/license/certs/types.ts - CertificateDefinitions.FULL_FORM = aes-256-gcm+ed25519

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.