ADR-0001. Chứng chỉ license ký Ed25519, xác minh offline qua Redis
| Trường | Giá trị |
|---|---|
| Status | Accepted |
| Date | 2026-03-20 |
| Deciders | licensing-team, platform-team |
| Supersedes | — |
Bối cảnh
- Mọi request trong mọi service (sale, commerce, inventory, …) cần biết trạng thái license, tier, và feature của một merchant / user.
- Một lệnh gọi HTTP đồng bộ tới licensing trên mỗi request sẽ ràng mọi service vào tính khả dụng của licensing và thêm latency vào hot path.
- Trạng thái license thay đổi hiếm (issue / suspend / renew / revoke), nhưng đọc liên tục — hồ sơ read-heavy, write-rare.
- Dữ liệu phải chống giả mạo (consumer không được giả mạo payload "PERPETUAL/all-features") và bảo mật (config feature không nên đọc được từ một dump Redis 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 nó offline qua LicenseMiddleware của @nx/core — họ không bao giờ gọi licensing lúc runtime.
Envelope chứng chỉ là aes-256-gcm+ed25519:
ILicenseCertificatePayload(status, tier, features, giới hạn activation, expiry) được mã hoá AES-256-GCM vớiAPP_ENV_APPLICATION_SECRET.- Ciphertext được ký Ed25519 với
APP_ENV_LICENSING_ED25519_PRIVATE_KEY; consumer xác minh với public key đóng gói trong@nx/core. - Payload mang
certExpiresAt; key Redis cũng nhậnEX = APP_ENV_LICENSING_CERT_TTL_SECONDS(mặc định 86400).
Licensing re-publish sau mỗi mutation vòng đời (issue / suspend / reinstate / renew / revoke) và khi lazy expiry. LicenseMiddleware là fail-open: cert thiếu / hết hạn / không xác minh được trả về license context null ("unknown", không phải "unlicensed").
Hệ quả
| Ưu | Nhược |
|---|---|
| Zero ràng buộc runtime — consumer không gọi licensing | Crypto bất đối xứng + AES mỗi lần publish license (rẻ, nhưng có thật) |
Đường read là một Redis GET + verify cục bộ | APP_ENV_APPLICATION_SECRET dùng chung phải phân phối tới mọi verifier (lan tràn secret đối xứng) |
| Chống giả mạo (Ed25519) + bảo mật (AES) | Xoay keypair yêu cầu roll public-key phối hợp + re-publish |
| Trạng thái cũ giới hạn bởi TTL ngay cả khi bỏ lỡ publish | Sửa feature qua CRUD không re-publish — cert có thể trễ so với DB tới khi có hành động vòng đời |
| Fail-open tránh việc licensing down kéo sập mọi service | Fail-open nghĩa Redis down lặng lẽ làm suy giảm cưỡng chế — cổng phải xử lý null có chủ ý |
Phương án đã cân nhắc
| Phương án | Ưu | Nhược | Vì sao loại |
|---|---|---|---|
| HTTP validate đồng bộ mỗi request | Luôn tươi | Ràng mọi service vào uptime + latency của licensing | Chi phí hot-path & bán kính ảnh hưởng không chấp nhận được |
| JWT (bất đối xứng) thay vì envelope AES+Ed25519 | Chuẩn, nhiều công cụ | Payload đọc được (base64) — config feature lộ | Yêu cầu bảo mật |
| Chứng chỉ chỉ HMAC đối xứng | Đơn giản hơn, một secret | Bất kỳ consumer nào giữ secret đều giả mạo được cert | Cần tách tin cậy issuer/verifier |
| Broadcast Kafka thay đổi license | Push, event-sourced | Thêm phụ thuộc broker cho nhu cầu read-on-demand; consumer phải lưu trạng thái | Quá kỹ cho read-heavy/write-rare; cache Redis là đủ |
Tham chiếu
licensing/src/services/licensing/licensing-base.service.ts—publishCertificate()core/src/services/license/certs/signer.ts—LicenseCertSignerHelper.signcore/src/services/license/certs/verifier.ts—LicenseCertVerifierHelper.verifycore/src/middlewares/license/license.middleware.ts—LicenseProvider(fail-open)core/src/services/license/certs/types.ts—CertificateDefinitions.FULL_FORM = aes-256-gcm+ed25519