┌─────────────┐ 1. Bấm "Ký số MPKI" ┌──────────────────┐
│ Trình duyệt │ ──────────────────────────────▶ │ Backend Function │
│ (Frontend) │ │ mpkiSign │
│ React App │ │ (Base44 Runtime) │
└──────┬───────┘ └────────┬───────────┘
│ │ 2. Lấy access_token
│ │ (client_credentials)
│ ▼
│ ┌────────────────────┐
│ │ VGCA API (OAuth) │
│ │ /oauth/token │
│ └────────┬───────────┘
│ │ 3. Tạo giao dịch ký
│ ▼
│ ┌────────────────────┐
│ 4. Tạo bản ghi DigitalSignature │ VGCA API │
│ (trạng thái: Đang chờ) │ /api/v1/signing/ │
│ + callback_url = WEBHOOK_URL │ transactions │
│ └────────┬───────────┘
│ │
◀────────────── 5. Trả transactionId ────────────┘
│
│ 6. App VGCA-Sign trên điện thoại nhận thông báo → người ký duyệt
│ (polling dự phòng mỗi 5s + realtime subscription)
│
│ ┌────────────────────┐
│ 7. VGCA gọi Webhook về hệ thống│ VGCA Server │
│ POST /functions/mpkiWebhook │ (push kết quả ký) │
│ Header: X-VGCA-Signature └────────┬───────────┘
│ │
▼ ▼
┌─────────────┐ ┌────────────────────┐
│ Frontend │◀── realtime update ─────│ mpkiWebhook │
│ cập nhật │ (entity subscribe) │ - Verify HMAC │
│ trạng thái │ │ - Map trạng thái │
└─────────────┘ │ - completeSignature│
│ - →processSigning │
└────────────────────┘| Thành phần | Vai trò |
|---|---|
MpkiSignButton (Frontend) | Nút bấm khởi tạo phiên ký, hiển thị trạng thái, polling dự phòng |
mpkiSign (Backend function) | Tạo giao dịch ký trên VGCA + tạo bản ghi DigitalSignature |
mpkiCheckStatus (Backend function) | Polling trạng thái giao dịch từ VGCA (dự phòng khi webhook chậm) |
mpkiWebhook (Backend function) | Nhận callback từ VGCA, xác thực HMAC, hoàn tất phiên ký |
mpkiTestConnection (Backend function) | Kiểm tra kết nối VGCA (admin) |
mpki.ts (Shared module) | Logic dùng chung: auth, tạo/tra cứu giao dịch, map trạng thái, HMAC |
Entity DigitalSignature | Lưu trữ phiên ký + file đã ký + metadata chứng thư |
base44/shared/mpki.ts nên chỉ cần sửa 1 file.| Method | POST |
| Endpoint | {VGCA_MPKI_API_ENDPOINT}/oauth/token |
| Content-Type | application/json |
| Body | { "grant_type": "client_credentials", "client_id": "{CLIENT_ID}", "client_secret": "{CLIENT_SECRET}" } |
| Response | { "access_token": "...", "expires_in": ... } |
| Auth scope | Server-to-server (không liên quan user cuối) |
| Method | POST |
| Endpoint | {VGCA_MPKI_API_ENDPOINT}/api/v1/signing/transactions |
| Authorization | Bearer {access_token} |
| Body | { "file_url": "...", "document_title": "...", "signer_name": "...", "reference": "...", "callback_url": "https://hup-e-document-primary.base44.app/functions/mpkiWebhook" } |
| Response | { "transaction_id": "..." } |
| Method | GET |
| Endpoint | {VGCA_MPKI_API_ENDPOINT}/api/v1/signing/transactions/{transaction_id} |
| Response | { "status": "PENDING|COMPLETED|FAILED|REJECTED", "signed_file_url": "..." } |
Polling chạy mỗi 5 giây từ frontend, tối đa 5 phút (mpki_poll_expires_at), sau đó chỉ còn dựa vào webhook.
POST https://hup-e-document-primary.base44.app/functions/mpkiWebhook
X-VGCA-Signature (chữ ký HMAC do VGCA tạo).HMAC-SHA256(webhook_secret, raw_body) bằng VGCA_MPKI_WEBHOOK_SECRET.timingSafeEqualHex (hằng-thời gian, chống timing attack).401 Unauthorized.const expected = await hmacSha256Hex(config.webhookSecret, bodyText);
if (!timingSafeEqualHex(signature.toLowerCase(), expected.toLowerCase())) {
return Response.json({ error: 'Chữ ký webhook không hợp lệ' }, { status: 401 });
}{
"transaction_id": "VGCA-XXXX",
"status": "COMPLETED",
"signed_file_url": "https://...",
"failure_reason": "..."
}transaction_id → tìm bản ghi DigitalSignature tương ứng.Đã ký/Ký thất bại) → bỏ qua (idempotent, chống xử lý trùng).PENDING → "Đang chờ duyệt trên điện thoại"COMPLETED → "Đã ký"FAILED/REJECTED → "Ký thất bại"completeSignature:mpki_status, signed_file_url, signature_status = "Hợp lệ", signed_at.processSigningApproval để hoàn tất luồng trình ký nội bộ.Webhook và polling không xung đột: nếu webhook đến trước, polling thấy terminal=true sẽ dừng; nếu polling đến trước, webhook thấy trạng thái đã kết thúc sẽ skipped. Polling là cơ chế dự phòng (fallback) đảm bảo hệ thống vẫn cập nhật ngay cả khi VGCA không gọi webhook hoặc webhook bị mất.
Toàn bộ credentials VGCA lưu trong Secrets của app Base44 (không hardcode trong code):
| Tên secret | Mô tả |
|---|---|
VGCA_MPKI_CLIENT_ID | Client ID do Ban Cơ yếu cấp |
VGCA_MPKI_CLIENT_SECRET | Client Secret do Ban Cơ yếu cấp |
VGCA_MPKI_API_ENDPOINT | Base URL API VGCA (vd: https://api.vgca.gov.vn) |
VGCA_MPKI_WEBHOOK_SECRET | Secret dùng verify chữ ký HMAC webhook |
Khi chưa có credentials thật (isMock = true), hệ thống tự chạy chế độ thử nghiệm: phiên ký tự "Đã ký" sau 10 giây, toàn bộ luồng UI/luồng trình ký hoạt động bình thường — cho phép UAT ngay lập tức mà không cần chờ key. Khi điền key vào Secrets, hệ thống tự động chuyển sang production.
base44.auth.me()) mới được khởi tạo phiên ký.mpkiTestConnection chỉ admin (admin/admin_hethong) được gọi.Mỗi phiên ký tạo 1 bản ghi, lưu trữ đầy đủ metadata:
| Trường | Mô tả |
|---|---|
mpki_transaction_id | Mã giao dịch VGCA (tra cứu) |
mpki_status | Chờ ký / Đang chờ duyệt trên điện thoại / Đã ký / Ký thất bại |
mpki_provider | "VGCA-MPKI" |
mpki_initiated_at / mpki_completed_at | Thời điểm tạo / hoàn tất |
mpki_failure_reason | Lý do thất bại / hủy |
mpki_poll_expires_at | Hạn polling dự phòng |
original_file_url | Link file PDF gốc (trước ký) |
signed_file_url | Link file PDF đã ký (sau ký) |
signer_name / signer_id / signer_role | Thông tin người ký |
document_id / document_type / document_title / document_number | Văn bản liên quan |
signing_request_id | Liên kết phiếu trình ký nội bộ |
source_entity_type / source_entity_id | Entity gốc chứa file PDF |
certificate_serial / certificate_issuer | Thông tin chứng thư số |
signature_status | Hợp lệ / Hết hạn / Bị thu hồi / Chờ xác thực |
signed_at | Thời điểm ký thành công |
Hệ thống triển khai luồng trình ký đa cấp theo mô hình trường đại học:
Người soạn thảo → Trưởng phòng → Phó Hiệu trưởng → Hiệu trưởng (Draft) (Cấp 0) (Cấp 1) (Cấp 2 — cuối)
Mỗi cấp ký tạo một SigningRequest. Các phiếu cùng một văn bản được gom nhóm bằng chain_key và sắp xếp theo level (0, 1, 2...). Tại mỗi cấp, người ký có 3 lựa chọn: Ký duyệt (approve), Từ chối (reject), hoặc Yêu cầu bổ sung (request_more).
┌─────────────────────────────────────────────────────────────────┐
│ NGƯỜI SOẠN THẢO (Văn thư / Cán bộ đơn vị) │
│ 1. Soạn thảo dự thảo (DocumentDraft) │
│ 2. Chọn chuỗi ký: Trưởng phòng → Phó HT → HT │
│ 3. Đính kèm file PDF + nộp trình ký │
│ → Tạo SigningRequest cấp 0 (Chờ ký) │
│ + SigningRequest cấp 1, 2 (Chờ duyệt — xếp hàng) │
│ (cùng chain_key, level 0/1/2) │
└──────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ TRƯỞNG PHÒNG (Cấp 0 — signer_role: "Trưởng phòng") │
│ Nhận thông báo → xem văn bản │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Ký duyệt │ │ Từ chối │ │ Yêu cầu bổ sung │ │
│ │ (approve) │ │ (reject) │ │ (request_more) │ │
│ └──────┬──────┘ └──────┬───────┘ └──────────┬───────────┘ │
└─────────┼────────────────┼─────────────────────┼────────────────┘
│ │ │
[approve] [reject] [request_more]
│ │ │
│ ┌───────────┘ │
│ │ Hủy toàn bộ cấp còn lại │ Trả draft về
│ │ Draft → "Đang soạn" │ "Đang soạn"
│ │ Thông báo người trình │
│ └──────────────────────── │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ processSigningApproval (Backend) │
│ • Tạo DigitalSignature (certificate_serial, signature_hash) │
│ • Tìm cấp kế tiếp (Chờ duyệt, level > hiện tại) │
│ → chuyển sang "Chờ ký" + thông báo người ký kế tiếp │
│ • Nếu là cấp CUỐI → finalise văn bản gốc: │
│ - OutgoingDocument → "Đã vào sổ" │
│ - InternalDocument → "Đã phát hành" │
│ - DocumentDraft → "Đã trình ký" │
│ + Email Outlook cho người trình + thông báo admin │
└──────────────────────┬──────────────────────────────────────────┘
│ (cấp kế tiếp = Chờ ký)
▼
┌─────────────────────────────────────────────────────────────────┐
│ PHÓ HIỆU TRƯỞNG (Cấp 1 — signer_role: "Phó Hiệu trưởng") │
│ Cùng luồng: approve / reject / request_more │
│ → approve → chuyển HT (cấp 2) │
└──────────────────────┬──────────────────────────────────────────┘
│ (cấp kế tiếp = Chờ ký)
▼
┌─────────────────────────────────────────────────────────────────┐
│ HIỆU TRƯỞNG (Cấp 2 — signer_role: "Hiệu trưởng" — CẤP CUỐI) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ KÝ SỐ VGCA │ │
│ │ • Bấm "Ký số MPKI" → mpkiSign → VGCA tạo transaction │ │
│ │ • App VGCA-Sign trên điện thoại → duyệt │ │
│ │ • Webhook/polling → completeSignature │ │
│ │ • processSigningApproval (approve) — cấp cuối: │ │
│ │ - Tạo DigitalSignature (file đã ký) │ │
│ │ - Finalise văn bản gốc ("Đã vào sổ" / "Đã phát hành") │ │
│ │ - Email + thông báo hoàn thành chuỗi ký │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘| Trạng thái | Ý nghĩa |
|---|---|
Chờ ký | Đang chờ cấp này ký (cấp hiện tại active) |
Chờ duyệt | Xếp hàng chờ cấp trước ký xong (chưa đến lượt) |
Đã ký | Cấp này đã ký duyệt |
Từ chối | Cấp này từ chối ký |
Yêu cầu bổ sung | Cấp này yêu cầu bổ sung thông tin |
Khi nộp trình ký, người soạn chọn kế hoạch chuỗi ký — lưu dạng JSON trong trường signing_chain:
[
{ "role": "Trưởng phòng", "signer_id": "user-001", "signer_name": "Nguyễn Văn A" },
{ "role": "Phó Hiệu trưởng", "signer_id": "user-002", "signer_name": "Trần Thị B" },
{ "role": "Hiệu trưởng", "signer_id": "user-003", "signer_name": "Lê Văn C" }
]Hệ thống tự tạo N bản ghi SigningRequest (N = số cấp), cùng chain_key, level 0→N-1. Cấp 0 = Chờ ký, các cấp còn lại = Chờ duyệt.
Khi người ký ở cấp cuối (thường là Hiệu trưởng) chọn "Ký số MPKI" thay vì "Ký duyệt" thủ công:
mpkiSign tạo giao dịch VGCA → bản ghi DigitalSignature liên kết signing_request_id.completeSignature → cập nhật file đã ký.completeSignature tự gọi processSigningApproval(action="approve") → finalise văn bản gốc + email + thông báo.Điều này đảm bảo chữ ký số VGCA và luồng phê duyệt nội bộ liên thông — không cần thao tác thủ công thứ hai.
| Hạng mục | Trạng thái |
|---|---|
| Backend functions (mpkiSign, mpkiCheckStatus, mpkiWebhook, mpkiTestConnection, processSigningApproval) | ✅ Đã viết |
| Shared module mpki.ts (auth, tạo/tra cứu giao dịch, HMAC) | ✅ Đã viết |
| Frontend (nút Ký số MPKI, dialog trạng thái, polling, realtime) | ✅ Đã viết |
| Entity DigitalSignature + SigningRequest | ✅ Đã tạo |
| Luồng trình ký đa cấp (Trưởng phòng → Phó HT → HT) | ✅ Đã triển khai |
| Chế độ Mock (test UI không cần key) | ✅ Đang chạy |
| Webhook endpoint + xác thực HMAC | ✅ Đã viết |
| Panel quản trị (SystemAdmin → MPKI) | ✅ Đã viết |
| USB Token signing — plugin localhost API + UsbTokenSignPanel | ✅ Đã viết (v2.0) |
| Backend functions: usbTokenSign, checkCertificateStatus, getTimestampToken | ✅ Đã viết (v2.0) |
| Shared module: certificateValidation.ts (OCSP/CRL + TSA) | ✅ Đã viết (v2.0) |
| Entity DigitalSignature: signing_method, certificate_status, tsp_token... | ✅ Đã cập nhật (v2.0) |
| Panel quản trị: cấu hình USB Port, OCSP, CRL, TSA + nút Test | ✅ Đã viết (v2.0) |
mpki.ts.https://hup-e-document-primary.base44.app/functions/mpkiWebhook tại VGCA (nếu VGCA yêu cầu whitelist)./health, /certificates, /sign (request/response format).Sau khi có thông tin, admin cấu hình tại SystemAdmin → MPKI → Cấu hình USB Token & Kiểm tra chứng thư → bấm Test từng dịch vụ → toàn bộ hệ thống chuyển sang production.
Hệ thống hỗ trợ song song 2 phương thức ký số ở cấp cuối (Hiệu trưởng): MPKI remote (ký qua app điện thoại — Phần A) và USB Token (ký tại chỗ — Phần D). Người ký tự chọn phương thức phù hợp.
┌─────────────┐ ┌─────────────────────┐
│ Trình duyệt │ 1. Bấm "Ký bằng USB Token" │ VGCA Sign Tool │
│ (Frontend) │ ─────────────────────────────▶│ (Plugin cục bộ) │
│ React App │ │ http://127.0.0.1 │
└──────┬───────┘ │ :{VGCA_USB_LOCAL_PORT} │
│ └────────┬────────────┘
│ 2. GET /api/health (kiểm tra plugin) │
│ 3. GET /api/certificates (đọc chứng thư) │
│ 4. POST /api/sign (truyền file_url + cert + PIN) │
│ │
│ ┌────────┴────────────┐
│ │ USB Token (PKCS#11) │
│ │ hoặc CSP │
│ └────────┬────────────┘
│ │ 5. Ký file PDF
│ │ tại chỗ
│ ▼
│ 6. Trả về file đã ký (base64 hoặc URL) ┌────────────────────┐
│◀──────────────────────────────────────────│ File PDF đã ký │
│ └────────────────────┘
│
│ 7. Upload file đã ký lên lưu trữ (UploadPublicFile)
│ 8. Gọi usbTokenSign (backend)
│ → Tạo DigitalSignature (signing_method='usb_token')
│ → Kiểm tra chứng thư (OCSP/CRL) — Phần E
│ → Lấy dấu thời gian (TSA) — Phần F
│ → Gọi processSigningApproval (finalise văn bản)
▼
┌────────────────────┐
│ Văn bản gốc │
│ → "Đã vào sổ" │
│ → Email + thông báo│
└────────────────────┘| Endpoint | Method | Mô tả |
|---|---|---|
/api/health | GET | Kiểm tra plugin đang chạy — trả về { "running": true, "version": "1.x", "name": "VGCA Sign Tool" } |
/api/certificates | GET | Đọc danh sách chứng thư từ USB Token — trả về { "certificates": [{ "serial", "subject", "issuer", "validTo" }] } |
/api/sign | POST | Ký file PDF — body: { "file_url", "certificate_serial", "pin" } → trả về { "signed_file_base64" | "signed_file_url", "certificate_serial", "certificate_issuer", "plugin_version" } |
Frontend gọi plugin qua http://127.0.0.1:{VGCA_USB_LOCAL_PORT}/api/... — port cấu hình trong SystemConfig (mặc định 31568). Timeout: 3s (health), 5s (certificates), 120s (sign).
/api/health. Nếu chưa chạy → hiển thị hướng dẫn tải & cài đặt plugin./api/certificates → hiển thị danh sách chứng thư → người ký chọn.checkCertificateStatus (backend) — xem Phần E. Nếu bị thu hồi/hết hạn → chặn ký./api/sign → plugin tải file PDF từ file_url, ký bằng USB Token, trả về file đã ký.usbTokenSign (backend) → tạo DigitalSignature + TSA + processSigningApproval.VGCA_USB_LOCAL_PORT) cấu hình trong SystemConfig — admin chỉnh trong SystemAdmin → MPKI.http (không HTTPS) vì cùng máy — không có risk MITM.Trước khi ký (cả MPKI remote và USB Token), hệ thống kiểm tra chứng thư số chưa bị thu hồi và chưa hết hạn qua OCSP (Online Certificate Status Protocol — RFC 6960) hoặc CRL (Certificate Revocation List — RFC 5280). Yêu cầu theo Điều 28, Nghị định 130/2018/NĐ-CP.
| Function | checkCertificateStatus |
| Input | { certificateSerial, certificateIssuer?, signatureId? } |
| Output | { status: 'good'|'revoked'|'expired'|'unknown', checked_at, raw_response, mock } |
| Auth | Người dùng đã đăng nhập |
| Endpoint | {VGCA_OCSP_ENDPOINT} |
| Method | POST |
| Content-Type | application/json |
| Body | { "serial": "...", "issuer": "..." } |
| Response | { "status": "good|revoked|expired|unknown" } |
| Endpoint | {VGCA_CRL_ENDPOINT} |
| Method | GET |
| Response | Danh sách chứng thư bị thu hồi (text/PEM/DER) — kiểm tra certificateSerial có trong danh sách không |
Khi chưa cấu hình OCSP/CRL endpoint → hệ thống tự chạy mock mode: giả định chứng thư hợp lệ (status='good'), cho phép test toàn bộ luồng UI. Khi admin điền endpoint vào SystemConfig → tự động chuyển sang production.
Kết quả kiểm tra lưu vào DigitalSignature:
| Trường | Mô tả |
|---|---|
certificate_status | good / revoked / expired / unknown |
certificate_checked_at | Thời điểm kiểm tra (ISO 8601) |
ocsp_response | Phản hồi raw (JSON/text) — truy vết |
Sau khi ký thành công, hệ thống lấy dấu thời gian tin cậy (Timestamp Token) từ dịch vụ TSA của VGCA theo chuẩn RFC 3161. Đảm bảo tính pháp lý dài hạn — không phụ thuộc thời gian server nội bộ. Yêu cầu theo Điều 28, Nghị định 130/2018/NĐ-CP.
| Function | getTimestampToken |
| Input | { fileHash, signatureId? } |
| Output | { tsp_token (base64), tsp_signed_at, mock } |
| Auth | Người dùng đã đăng nhập |
| Endpoint | {VGCA_TSA_ENDPOINT} |
| Method | POST |
| Content-Type | application/timestamp-query |
| Body | { "hash": "...", "hashAlgorithm": "SHA-256" } |
| Response | { "token": "...", "timestamp": "..." } — Timestamp Token (RFC 3161) |
Khi chưa cấu hình TSA endpoint → hệ thống tự tạo mock timestamp token (base64 giả), cho phép test luồng. Khi admin điền endpoint → tự động chuyển sang production.
| Trường | Mô tả |
|---|---|
tsp_token | Dấu thời gian tin cậy (base64) — Timestamp Token theo RFC 3161 |
tsp_signed_at | Thời điểm TSA cấp token (ISO 8601) — không phụ thuộc server nội bộ |
Function usbTokenSign tự động gọi getTimestampToken sau khi tạo DigitalSignature — không cần thao tác thủ công. Nếu TSA lỗi, chữ ký vẫn được ghi nhận (TSA là bổ sung pháp lý, không chặn flow ký).