---
url: 'https://www.corbado.com/vi/blog/cach-xay-dung-verifiable-credential-verifier'
title: 'Cách xây dựng Trình xác minh thông tin xác thực kỹ thuật số (Hướng dẫn cho nhà phát triển)'
description: 'Tìm hiểu cách xây dựng một trình xác minh thông tin xác thực kỹ thuật số từ đầu bằng Next.js, OpenID4VP và ISO mDoc. Hướng dẫn chi tiết này sẽ chỉ cho bạn cách tạo một trình xác minh có thể yêu cầu, nhận và xác thực giấy phép lái xe di động cùng các thông'
lang: 'vi'
author: 'Amine'
date: '2025-08-20T15:39:37.402Z'
lastModified: '2026-03-25T10:08:33.863Z'
keywords: 'trình xác minh thông tin xác thực kỹ thuật số, hướng dẫn xây dựng trình xác minh, xây dựng trình xác minh'
category: 'Digital Credentials'
---

# Cách xây dựng Trình xác minh thông tin xác thực kỹ thuật số (Hướng dẫn cho nhà phát triển)

## 1. Giới thiệu

Chứng minh danh tính trực tuyến là một thách thức không ngừng, dẫn đến sự phụ thuộc vào
mật khẩu và việc chia sẻ các tài liệu nhạy cảm qua các kênh không an toàn. Điều này đã làm
cho quy trình xác minh danh tính của doanh nghiệp trở nên chậm chạp, tốn kém và dễ bị gian
lận. Thông tin xác thực kỹ thuật số (Digital Credentials) mang đến một cách tiếp cận mới,
trao lại quyền kiểm soát dữ liệu cho người dùng. Chúng là phiên bản kỹ thuật số của một
[wallet](https://www.corbado.com/blog/digital-wallet-assurance) vật lý, chứa mọi thứ từ giấy phép lái xe đến bằng
đại học, nhưng có thêm lợi ích là an toàn về mặt mật mã, bảo vệ quyền riêng tư và có thể
xác minh ngay lập tức.

Guide này cung cấp cho các nhà phát triển một
[hướng dẫn](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql) thực tế, từng bước để xây dựng một
trình xác minh cho [Digital Credentials](https://www.corbado.com/blog/digital-credentials-api). Mặc dù các tiêu
chuẩn đã tồn tại, nhưng có rất ít [hướng dẫn](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql)
về việc triển khai chúng. [Hướng dẫn](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql) này lấp
đầy khoảng trống đó, chỉ cho bạn cách xây dựng một trình xác minh sử dụng API Digital
Credential gốc của trình duyệt, [OpenID4VP](https://www.corbado.com/glossary/open-id-4-vp) cho giao thức trình
bày và ISO [mDoc](https://www.corbado.com/glossary/mdoc) (ví dụ: giấy phép lái xe di động) làm định dạng thông
tin xác thực.

Kết quả cuối cùng sẽ là một ứng dụng [Next.js](https://www.corbado.com/blog/nextjs-passkeys) đơn giản nhưng đầy
đủ chức năng, có thể yêu cầu, nhận và xác minh một Digital Credential từ một
[wallet](https://www.corbado.com/blog/digital-wallet-assurance) di động tương thích.

Dưới đây là cái nhìn nhanh về ứng dụng cuối cùng khi hoạt động. Quá trình này bao gồm bốn
bước chính:

**Bước 1: Trang ban đầu** Người dùng truy cập trang ban đầu và nhấp vào "Xác minh với
[Digital Identity](https://www.corbado.com/blog/digital-identity-guide)" để bắt đầu quá trình.
![Trang ban đầu cho yêu cầu xác minh](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Screenshot_2025_07_25_at_11_00_33_5217b35c96.png)

**Bước 2: Lời nhắc tin cậy** Trình duyệt yêu cầu người dùng xác nhận tin cậy. Người dùng
nhấp vào "Tiếp tục" để tiến hành.
![Lời nhắc tin cậy của trình duyệt](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Screenshot_2025_07_25_at_11_00_39_ba390a8097.png)

**Bước 3: Quét mã QR** Một [mã QR](https://www.corbado.com/vi/blog/phuong-phap-dang-nhap-xac-thuc-ma-qr) được
hiển thị, người dùng quét mã này bằng ứng dụng [wallet](https://www.corbado.com/blog/digital-wallet-assurance)
tương thích của họ.
![Mã QR để quét](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Screenshot_2025_07_25_at_11_00_45_3b3066a10.png)

**Bước 4: Thông tin xác thực đã được giải mã** Sau khi xác minh thành công, ứng dụng hiển
thị dữ liệu thông tin xác thực đã được giải mã.
![Kết quả thông tin xác thực đã được giải mã](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Screenshot_2025_07_25_at_11_01_36_684f7489cd.png)

### 1.1 Cách hoạt động

Điều kỳ diệu đằng sau thông tin xác thực kỹ thuật số nằm ở mô hình "**tam giác tin cậy**"
đơn giản nhưng mạnh mẽ, liên quan đến ba bên chính:

- **Bên phát hành (Issuer):** Một cơ quan có thẩm quyền đáng tin cậy (ví dụ: một cơ quan
  [chính phủ](https://www.corbado.com/passkeys-for-public-sector), trường đại học, hoặc ngân hàng) ký và cấp
  thông tin xác thực bằng mật mã cho người dùng.
- **Người nắm giữ (Holder):** Người dùng, người nhận thông tin xác thực và lưu trữ nó một
  cách an toàn trong một [digital wallet](https://www.corbado.com/blog/digital-wallet-assurance) cá nhân trên
  thiết bị của họ.
- **Bên xác minh (Verifier):** Một ứng dụng hoặc dịch vụ cần kiểm tra thông tin xác thực
  của người dùng.

![Hệ sinh thái W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/diagrams/ecosystem.svg)

Khi người dùng muốn truy cập một dịch vụ, họ trình bày thông tin xác thực từ wallet của
mình. Bên xác minh sau đó có thể kiểm tra ngay lập tức tính xác thực của nó mà không cần
liên hệ trực tiếp với bên phát hành ban đầu.

### 1.2 Tại sao Trình xác minh lại cần thiết (và Tại sao bạn lại ở đây)

Để **hệ sinh thái danh tính phi tập trung** này phát triển mạnh, vai trò của **trình xác
minh (verifier)** là cực kỳ quan trọng. Họ là những người gác cổng của
[cơ sở hạ tầng](https://www.corbado.com/passkeys-for-critical-infrastructure) tin cậy mới này, là những người sử
dụng thông tin xác thực và làm cho chúng trở nên hữu ích trong thế giới thực. Như sơ đồ
dưới đây minh họa, một trình xác minh hoàn thành tam giác tin cậy bằng cách yêu cầu, nhận
và xác thực thông tin xác thực từ người nắm giữ.

Nếu bạn là một nhà phát triển, việc xây dựng một dịch vụ để thực hiện việc xác minh này là
một kỹ năng nền tảng cho thế hệ ứng dụng an toàn và lấy người dùng làm trung tâm tiếp
theo. Guide này được thiết kế để hướng dẫn bạn qua chính xác quy trình đó. Chúng ta sẽ đề
cập đến mọi thứ bạn cần biết để **xây dựng trình xác minh thông tin xác thực có thể xác
minh của riêng bạn**, từ các khái niệm và tiêu chuẩn cốt lõi đến các chi tiết triển khai
từng bước về việc xác thực chữ ký và kiểm tra trạng thái thông tin xác thực.

> **Bạn muốn bỏ qua phần trước?** Bạn có thể tìm thấy dự án hoàn chỉnh cho hướng dẫn này
> trên GitHub. Hãy thoải mái sao chép nó và tự mình thử nghiệm:
> [https://github.com/corbado/digital-credentials-example](https://github.com/corbado/digital-credentials-example)

Hãy bắt đầu nào.

## 2. Các điều kiện tiên quyết để xây dựng Trình xác minh

Trước khi bắt đầu, hãy đảm bảo bạn có:

1. **Hiểu biết cơ bản về Digital Credentials và mdoc**
    - Hướng dẫn này tập trung vào định dạng **ISO mDoc** (ví dụ: cho giấy phép lái xe di
      động) và không đề cập đến các định dạng khác như W3C
      [Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) (VCs). Việc quen thuộc với các
      khái niệm cơ bản của [mdoc](https://www.corbado.com/glossary/mdoc) sẽ rất hữu ích.
2. **Docker và Docker Compose**
    - Dự án của chúng ta sử dụng cơ sở dữ liệu
      [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) trong một Docker container để quản lý
      trạng thái phiên OIDC. Hãy đảm bảo bạn đã cài đặt và chạy cả hai.
3. **Giao thức đã chọn: OpenID4VP**
    - Chúng ta sẽ sử dụng giao thức **OpenID4VP** (OpenID for Verifiable Presentations)
      cho luồng trao đổi thông tin xác thực.
4. **Chuẩn bị Tech Stack**
    - Sử dụng **TypeScript** (Node.js) cho logic backend.
    - Sử dụng **Next.js** cho cả backend (API routes) và frontend (UI).
    - Các thư viện chính: thư viện giải mã [CBOR](https://www.corbado.com/glossary/cbor) để phân tích
      [mdoc](https://www.corbado.com/glossary/mdoc) và một client [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide).
5. **Thông tin xác thực và Wallet thử nghiệm**
    - Chúng ta sẽ sử dụng
      **[CMWallet](https://github.com/digitalcredentialsdev/CMWallet/actions/runs/16407676816/artifacts/3574255220)**
      cho [Android](https://www.corbado.com/blog/how-to-enable-passkeys-android), một wallet hiểu các yêu cầu
      [OpenID4VP](https://www.corbado.com/glossary/open-id-4-vp) và có thể trình bày thông tin xác thực mdoc.
6. **Kiến thức cơ bản về Mật mã học**
    - Hiểu về chữ ký số và các khái niệm khóa công khai/khóa riêng tư liên quan đến mdoc
      và các luồng OIDC.

---

Bây giờ chúng ta sẽ đi qua từng điều kiện tiên quyết này một cách chi tiết, bắt đầu với
các tiêu chuẩn và giao thức làm nền tảng cho trình xác minh dựa trên mdoc này.

### 2.1 Lựa chọn Giao thức

Trình xác minh của chúng ta được xây dựng cho các tiêu chuẩn/giao thức sau:

| Tiêu chuẩn / Giao thức                                                                    | Mô tả                                                                                                                                                                                                                          |
| :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[W3C VC](https://www.w3.org/TR/vc-data-model/)**                                        | Mô hình Dữ liệu Thông tin xác thực có thể xác minh của W3C. Nó định nghĩa cấu trúc tiêu chuẩn cho thông tin xác thực kỹ thuật số, bao gồm các claim, metadata và bằng chứng.                                                   |
| **[SD-JWT](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/)** | Tiết lộ có chọn lọc cho JWT. Một định dạng cho VC dựa trên JSON Web Tokens cho phép người nắm giữ chỉ tiết lộ có chọn lọc các claim cụ thể từ một thông tin xác thực, tăng cường quyền riêng tư.                               |
| **[ISO mDoc](https://www.iso.org/standard/69084.html)**                                   | ISO/IEC 18013-5. Tiêu chuẩn quốc tế cho Giấy phép lái xe di động (mDLs) và các loại ID di động khác, định nghĩa cấu trúc dữ liệu và giao thức truyền thông cho việc sử dụng ngoại tuyến và trực tuyến.                         |
| **OpenID4VP**                                                                             | OpenID for Verifiable Presentations. Một giao thức trình bày có khả năng tương tác được xây dựng trên OAuth 2.0. Nó định nghĩa cách một trình xác minh yêu cầu thông tin xác thực và wallet của người nắm giữ trình bày chúng. |

Đối với hướng dẫn này, chúng ta đặc biệt sử dụng:

- **OpenID4VP** làm giao thức để yêu cầu và nhận thông tin xác thực.
- **ISO mDoc** làm định dạng thông tin xác thực (ví dụ: cho giấy phép lái xe di động).

> **Lưu ý về phạm vi:** Mặc dù chúng ta giới thiệu ngắn gọn về W3C VC và SD-JWT để cung
> cấp bối cảnh rộng hơn, hướng dẫn này chỉ triển khai thông tin xác thực ISO mDoc qua
> [OpenID4VP](https://www.corbado.com/glossary/open-id-4-vp). Các VC dựa trên W3C nằm ngoài phạm vi của ví dụ
> này.

#### 2.1.1 ISO mDoc (Mobile Document)

Tiêu chuẩn ISO/IEC 18013-5 mDoc định nghĩa cấu trúc và mã hóa cho các tài liệu di động như
giấy phép lái xe di động (mDLs). Thông tin xác thực mDoc được mã hóa bằng
[CBOR](https://www.corbado.com/glossary/cbor), được ký bằng mật mã và có thể được trình bày kỹ thuật số để xác
minh. Trình xác minh của chúng ta sẽ tập trung vào việc giải mã và xác thực các thông tin
xác thực mdoc này.

#### 2.1.2 OpenID4VP (OpenID for Verifiable Presentations)

OpenID4VP là một giao thức có khả năng tương tác để yêu cầu và trình bày thông tin xác
thực kỹ thuật số, được xây dựng trên [OAuth 2.0](https://www.corbado.com/glossary/oauth2) và OpenID Connect.
Trong triển khai này, OpenID4VP được sử dụng để:

- Khởi tạo luồng trình bày thông tin xác thực (qua
  [mã QR](https://www.corbado.com/vi/blog/phuong-phap-dang-nhap-xac-thuc-ma-qr) hoặc API trình duyệt)
- Nhận thông tin xác thực mdoc từ wallet của người dùng
- Đảm bảo việc trao đổi thông tin [xác thực an toàn](https://www.corbado.com/vi/glossary/open-id-4-vp), có trạng
  thái và bảo vệ quyền riêng tư

### 2.2 Lựa chọn Tech Stack

Bây giờ chúng ta đã hiểu rõ về các tiêu chuẩn và giao thức, chúng ta cần chọn tech stack
phù hợp để xây dựng trình xác minh của mình. Các lựa chọn của chúng ta được thiết kế để
đảm bảo sự mạnh mẽ, trải nghiệm nhà phát triển tốt và khả năng tương thích với hệ sinh
thái web hiện đại.

#### 2.2.1 Ngôn ngữ: TypeScript

Chúng ta sẽ sử dụng **TypeScript** cho cả mã frontend và backend. Là một tập hợp con mở
rộng của JavaScript, nó thêm vào kiểu tĩnh, giúp phát hiện lỗi sớm, cải thiện chất lượng
mã và làm cho các ứng dụng phức tạp dễ quản lý hơn. Trong bối cảnh nhạy cảm về
[bảo mật](https://www.corbado.com/vi/blog/cach-bat-passkey-tren-android) như xác minh thông tin xác thực, an toàn
kiểu là một lợi thế lớn.

#### 2.2.2 Framework: Next.js

**Next.js** là framework chúng ta lựa chọn vì nó cung cấp một trải nghiệm tích hợp, liền
mạch để xây dựng các ứng dụng [full-stack](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql).

- **Đối với Frontend:** Chúng ta sẽ sử dụng [Next.js](https://www.corbado.com/blog/nextjs-passkeys) với
  [React](https://www.corbado.com/blog/react-passkeys) để xây dựng giao diện người dùng nơi quá trình xác minh
  được khởi tạo (ví dụ: hiển thị [mã QR](https://www.corbado.com/vi/blog/phuong-phap-dang-nhap-xac-thuc-ma-qr)).
- **Đối với Backend:** Chúng ta sẽ tận dụng **Next.js API Routes** để tạo các endpoint
  phía máy chủ. Các endpoint này chịu trách nhiệm tạo các yêu cầu OpenID4VP hợp lệ và hoạt
  động như `redirect_uri` để nhận và xác minh phản hồi cuối cùng từ CMWallet một cách an
  toàn.

#### 2.2.3 Các thư viện chính

Việc triển khai của chúng ta dựa vào một bộ thư viện cụ thể cho frontend và backend:

- **next**: Framework [Next.js](https://www.corbado.com/blog/nextjs-passkeys), được sử dụng cho cả backend API
  routes và frontend UI.
- **react** và **react-dom**: Cung cấp năng lượng cho giao diện người dùng frontend.
- **cbor-web**: Được sử dụng để giải mã thông tin xác thực mdoc được mã hóa bằng
  [CBOR](https://www.corbado.com/glossary/cbor) thành các đối tượng JavaScript có thể sử dụng được.
- **mysql2**: Cung cấp kết nối cơ sở dữ liệu
  [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) để lưu trữ các challenge và phiên xác
  minh.
- **uuid**: Một thư viện để tạo các chuỗi challenge (nonces) duy nhất.
- **@types/uuid**: Các kiểu TypeScript để tạo UUID.

> **Lưu ý về `openid-client`:** Các trình xác minh cấp sản xuất, tiên tiến hơn có thể sử
> dụng thư viện `openid-client` để xử lý trực tiếp giao thức OpenID4VP trên backend, cho
> phép các tính năng như `redirect_uri` động. Trong một luồng OpenID4VP do máy chủ điều
> khiển với `redirect_uri`, `openid-client` sẽ được sử dụng để phân tích và xác thực trực
> tiếp các phản hồi `vp_token`. Đối với hướng dẫn này, chúng ta đang sử dụng một luồng đơn
> giản hơn, qua trung gian trình duyệt mà không yêu cầu nó, làm cho quá trình dễ hiểu hơn.

Tech stack này đảm bảo một triển khai trình xác minh mạnh mẽ, an toàn về kiểu và có khả
năng mở rộng, tập trung vào API Digital Credential của trình duyệt và định dạng thông tin
xác thực ISO mDoc.

### 2.3 Lấy Wallet và Thông tin xác thực thử nghiệm

Để kiểm tra trình xác minh của bạn, bạn cần một wallet di động có thể tương tác với API
Digital Credential của trình duyệt.

Chúng ta sẽ sử dụng
**[CMWallet](https://github.com/digitalcredentialsdev/CMWallet/actions/runs/16407676816/artifacts/3574255220)**,
một wallet thử nghiệm mạnh mẽ, tuân thủ OpenID4VP cho
[Android](https://www.corbado.com/blog/how-to-enable-passkeys-android).

**Cách cài đặt CMWallet (Android):**

1. **Tải xuống tệp APK** bằng liên kết ở trên trực tiếp trên thiết bị
   [Android](https://www.corbado.com/blog/how-to-enable-passkeys-android) của bạn.
2. Mở **Cài đặt > Bảo mật** trên thiết bị của bạn.
3. Bật **"Cài đặt ứng dụng không rõ nguồn gốc"** cho trình duyệt bạn đã sử dụng để tải
   xuống tệp.
4. Tìm tệp APK đã tải xuống trong thư mục "Tải xuống" của bạn và nhấn vào nó để bắt đầu
   cài đặt.
5. Làm theo các hướng dẫn trên màn hình để hoàn tất cài đặt.
6. Mở CMWallet, và bạn sẽ thấy nó đã được tải sẵn các thông tin xác thực thử nghiệm, sẵn
   sàng cho luồng xác minh.

> **Lưu ý:** Chỉ cài đặt các tệp APK từ các nguồn bạn tin cậy. Liên kết được cung cấp là
> từ kho lưu trữ dự án chính thức.

### 2.4 Kiến thức về Mật mã học

Trước khi chúng ta đi sâu vào việc triển khai, điều cần thiết là phải hiểu các khái niệm
mật mã học làm nền tảng cho thông tin xác thực có thể xác minh. Đây là điều làm cho chúng
"có thể xác minh" và đáng tin cậy.

#### 2.4.1 Chữ ký số: Nền tảng của sự tin cậy

Về cơ bản, một [Verifiable Credential](https://www.corbado.com/glossary/verifiable-credential) là một tập hợp các
claim (như tên, ngày sinh, v.v.) đã được ký điện tử bởi một bên phát hành. Một chữ ký số
cung cấp hai đảm bảo quan trọng:

- **Tính xác thực:** Nó chứng minh rằng thông tin xác thực thực sự được tạo ra bởi bên
  phát hành chứ không phải bởi một kẻ mạo danh.
- **Tính toàn vẹn:** Nó chứng minh rằng thông tin xác thực đã không bị thay đổi hoặc giả
  mạo kể từ khi nó được ký.

#### 2.4.2 Mật mã hóa khóa công khai/khóa riêng tư

Chữ ký số được tạo bằng mật mã hóa khóa công khai/khóa riêng tư (còn gọi là mật
[mã hóa bất đối xứng](https://www.corbado.com/vi/blog/webauthn-pubkeycredparams-credentialpublickey)). Đây là
cách nó hoạt động trong bối cảnh của chúng ta:

1. **Bên phát hành có một cặp khóa:** một khóa riêng tư, được giữ bí mật, và một khóa công
   khai, được cung cấp cho mọi người (thường qua DID Document của họ).
2. **Ký:** Khi một bên phát hành tạo ra một thông tin xác thực, họ sử dụng **khóa riêng
   tư** của mình để tạo ra một chữ ký số duy nhất cho dữ liệu thông tin xác thực cụ thể
   đó.
3. **Xác minh:** Khi trình xác minh của chúng ta nhận được thông tin xác thực, nó sử dụng
   **khóa công khai** của bên phát hành để kiểm tra chữ ký. Nếu kiểm tra thành công, trình
   xác minh biết rằng thông tin xác thực là xác thực và không bị giả mạo. Bất kỳ thay đổi
   nào đối với dữ liệu thông tin xác thực sẽ làm cho chữ ký không hợp lệ.

> **Lưu ý về DID:** Trong hướng dẫn này, chúng ta không giải quyết các khóa của bên phát
> hành thông qua DID. Trong môi trường sản xuất, các bên phát hành thường sẽ công khai các
> khóa công khai của họ qua DID hoặc các điểm cuối có thẩm quyền khác, mà trình xác minh
> sẽ sử dụng để xác thực mật mã.

#### 2.4.3 Verifiable Credentials dưới dạng JWT

[Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) thường được định dạng dưới dạng
**JSON Web Tokens (JWTs)**. Một JWT là một cách nhỏ gọn, an toàn cho URL để biểu diễn các
claim được chuyển giữa hai bên. Một JWT đã ký (còn được gọi là JWS) có ba phần được phân
tách bằng dấu chấm (`.`):

- **Header:** Chứa metadata về token, như thuật toán ký được sử dụng (`alg`).
- **Payload:** Chứa các claim thực tế của
  [Verifiable Credential](https://www.corbado.com/glossary/verifiable-credential) (`vc` claim), bao gồm `issuer`,
  `credentialSubject`, v.v.
- **Signature:** Chữ ký số được tạo bởi bên phát hành, bao gồm cả header và payload.

```
// Ví dụ về cấu trúc JWT
[Header].[Payload].[Signature]
```

> **Lưu ý:** [Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) dựa trên JWT nằm ngoài
> phạm vi của bài đăng blog này. Việc triển khai này tập trung vào thông tin xác thực ISO
> mDoc và OpenID4VP, không phải W3C Verifiable Credentials hay thông tin xác thực dựa trên
> JWT.

#### 2.4.4 Verifiable Presentation: Chứng minh quyền sở hữu

Việc một trình xác minh biết rằng một thông tin xác thực là hợp lệ là chưa đủ; nó cũng cần
biết rằng người _trình bày_ thông tin xác thực là người nắm giữ hợp pháp. Điều này ngăn
chặn ai đó sử dụng một thông tin xác thực bị đánh cắp.

Điều này được giải quyết bằng cách sử dụng **Verifiable Presentation (VP)**. Một VP là một
lớp bọc xung quanh một hoặc nhiều VC được **ký bởi chính người nắm giữ**.

Luồng diễn ra như sau:

1. Trình xác minh yêu cầu người dùng trình bày một thông tin xác thực.
2. Wallet của người dùng tạo ra một
   [Verifiable Presentation](https://www.corbado.com/glossary/verifiable-presentation), gói (các) thông tin xác
   thực cần thiết vào bên trong nó, và ký toàn bộ bản trình bày bằng **khóa riêng tư của
   người nắm giữ**.
3. Wallet gửi VP đã ký này đến trình xác minh.

Trình xác minh của chúng ta sau đó phải thực hiện **hai** lần kiểm tra chữ ký riêng biệt:

1. **Xác minh (các) Credential:** Kiểm tra chữ ký trên mỗi VC bên trong bản trình bày bằng
   **khóa công khai của bên phát hành**. (Chứng minh thông tin xác thực là thật).
2. **Xác minh Presentation:** Kiểm tra chữ ký trên chính VP bằng **khóa công khai của
   người nắm giữ**. (Chứng minh người trình bày nó là chủ sở hữu).

Việc kiểm tra hai cấp độ này đảm bảo cả tính xác thực của thông tin xác thực và danh tính
của người trình bày nó, tạo ra một mô hình tin cậy mạnh mẽ và an toàn.

> **Lưu ý:** Khái niệm Verifiable Presentations như được định nghĩa trong hệ sinh thái W3C
> VC nằm ngoài phạm vi của bài đăng blog này. Thuật ngữ
> [Verifiable Presentation](https://www.corbado.com/glossary/verifiable-presentation) ở đây đề cập đến phản hồi
> `vp_token` của OpenID4VP, hoạt động tương tự như một W3C VP nhưng dựa trên ngữ nghĩa của
> ISO mDoc thay vì mô hình chữ ký [JSON-LD](https://www.corbado.com/glossary/json-ld) của W3C. Guide này tập
> trung vào thông tin xác thực ISO mDoc và OpenID4VP, không phải W3C Verifiable
> Presentations hay việc xác thực chữ ký của chúng.

## 3. Tổng quan về kiến trúc

Kiến trúc trình xác minh của chúng ta sử dụng **API Digital Credential** tích hợp của
trình duyệt làm trung gian an toàn để kết nối ứng dụng web của chúng ta với **CMWallet**
di động của người dùng. Cách tiếp cận này đơn giản hóa luồng bằng cách để trình duyệt xử
lý việc hiển thị mã QR gốc và giao tiếp với wallet.

- **Frontend (Next.js & React):** Một trang web nhẹ hướng tới người dùng. Công việc của nó
  là lấy một đối tượng yêu cầu từ backend của chúng ta, chuyển nó đến API
  `navigator.credentials.get()` của trình duyệt, nhận kết quả và chuyển tiếp nó đến
  backend để xác minh.
- **Backend (Next.js API Routes):** Trung tâm xử lý của trình xác minh. Nó tạo ra một đối
  tượng yêu cầu hợp lệ cho API trình duyệt và cung cấp một endpoint để nhận bản trình bày
  thông tin xác thực từ frontend để xác thực cuối cùng.
- **Trình duyệt (Credential API):** Người điều phối. Nó nhận đối tượng yêu cầu từ frontend
  của chúng ta, hiểu giao thức `openid4vp` và tự động tạo mã QR. Sau đó, nó chờ wallet trả
  về một phản hồi.
- **CMWallet (Ứng dụng di động):** Wallet của người dùng. Nó quét mã QR, xử lý yêu cầu,
  nhận được sự đồng ý của người dùng và gửi phản hồi đã ký trở lại trình duyệt.

Dưới đây là một sơ đồ tuần tự minh họa luồng hoàn chỉnh và chính xác:

![Luồng xác minh sử dụng API Digital Credentials của trình duyệt](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_24_233623_1b6ed9b957.svg)

**Giải thích luồng:**

1. **Khởi tạo:** Người dùng nhấp vào nút "Xác minh" trên **Frontend** của chúng ta.
2. **Đối tượng yêu cầu:** Frontend gọi đến **Backend** của chúng ta (`/api/verify/start`),
   backend này tạo một đối tượng yêu cầu chứa truy vấn và một nonce, sau đó trả về nó.
3. **Gọi API trình duyệt:** Frontend gọi `navigator.credentials.get()` với đối tượng yêu
   cầu.
4. **Mã QR gốc:** **Trình duyệt** thấy yêu cầu giao thức `openid4vp` và tự động hiển thị
   một mã QR. Lời hứa (promise) `.get()` bây giờ đang ở trạng thái chờ.

> **Lưu ý:** Luồng mã QR này xảy ra trên các trình duyệt máy tính để bàn. Trên các trình
> duyệt di động (Android Chrome với cờ thử nghiệm được bật), trình duyệt có thể giao tiếp
> trực tiếp với các wallet tương thích trên cùng một thiết bị, loại bỏ nhu cầu quét mã QR.
> Để bật tính năng này trên Android Chrome, hãy điều hướng đến
> `chrome://flags#web-identity-digital-credentials` và đặt cờ thành "Enabled".

5. **Quét & Trình bày:** Người dùng quét mã QR bằng **CMWallet**. Wallet nhận được sự chấp
   thuận của người dùng và gửi
   [Verifiable Presentation](https://www.corbado.com/glossary/verifiable-presentation) trở lại trình duyệt.
6. **Promise được giải quyết:** Trình duyệt nhận được phản hồi, và lời hứa `.get()` ban
   đầu trên frontend cuối cùng được giải quyết, cung cấp payload của bản trình bày.
7. **Xác minh Backend:** Frontend gửi **POST** payload của bản trình bày đến endpoint
   `/api/verify/finish` của backend. Backend xác thực nonce và thông tin xác thực.
8. **Kết quả:** Backend trả về một thông báo thành công hoặc thất bại cuối cùng cho
   frontend, frontend này sẽ cập nhật giao diện người dùng.

## 4. Xây dựng Trình xác minh

Bây giờ chúng ta đã hiểu rõ về các tiêu chuẩn, giao thức và luồng kiến trúc, chúng ta có
thể bắt đầu xây dựng trình xác minh của mình.

> **Làm theo hoặc sử dụng mã nguồn cuối cùng**
>
> Bây giờ chúng ta sẽ đi qua từng bước thiết lập và triển khai mã. Nếu bạn muốn chuyển
> thẳng đến sản phẩm hoàn chỉnh, bạn có thể sao chép dự án hoàn chỉnh từ kho lưu trữ
> GitHub của chúng tôi và chạy nó cục bộ.
>
> ```bash
> git clone https://github.com/corbado/digital-credentials-example.git
> ```

### 4.1 Thiết lập dự án

Đầu tiên, chúng ta sẽ khởi tạo một dự án Next.js mới, cài đặt các phụ thuộc cần thiết và
khởi động cơ sở dữ liệu của chúng ta.

#### 4.1.1 Khởi tạo ứng dụng Next.js

Mở terminal của bạn, điều hướng đến thư mục bạn muốn tạo dự án và chạy lệnh sau. Chúng ta
đang sử dụng App Router, TypeScript và Tailwind CSS cho dự án này.

```bash
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
```

Lệnh này tạo một ứng dụng Next.js mới trong thư mục hiện tại của bạn.

#### 4.1.2 Cài đặt các phụ thuộc

Tiếp theo, chúng ta cần cài đặt các thư viện sẽ xử lý việc giải mã CBOR, kết nối cơ sở dữ
liệu và tạo UUID.

```bash
npm install cbor-web mysql2 uuid @types/uuid
```

Lệnh này cài đặt:

- `cbor-web`: Để giải mã payload thông tin xác thực mdoc.
- `mysql2`: Client MySQL cho cơ sở dữ liệu của chúng ta.
- `uuid`: Để tạo các chuỗi challenge duy nhất.
- `@types/uuid`: Các kiểu TypeScript cho thư viện `uuid`.

#### 4.1.3 Khởi động cơ sở dữ liệu

Backend của chúng ta yêu cầu một cơ sở dữ liệu MySQL để lưu trữ dữ liệu phiên OIDC, đảm
bảo rằng mỗi luồng xác minh đều an toàn và có trạng thái. Chúng ta đã bao gồm một tệp
`docker-compose.yml` để làm cho việc này trở nên dễ dàng.

Nếu bạn đã sao chép kho lưu trữ, bạn chỉ cần chạy `docker-compose up -d`. Nếu bạn đang xây
dựng từ đầu, hãy tạo một tệp có tên `docker-compose.yml` với nội dung sau:

```yaml
services:
    mysql:
        image: mysql:8.0
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: rootpassword
            MYSQL_DATABASE: digital_credentials
            MYSQL_USER: app_user
            MYSQL_PASSWORD: app_password
        ports:
            - "3306:3306"
        volumes:
            - mysql_data:/var/lib/mysql
            - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
            timeout: 20s
            retries: 10

volumes:
    mysql_data:
```

Thiết lập Docker Compose này cũng yêu cầu một kịch bản khởi tạo SQL. Tạo một thư mục có
tên `sql` và bên trong nó, một tệp có tên `init.sql` với nội dung sau để thiết lập các
bảng cần thiết:

```sql
-- Create database if not exists
CREATE DATABASE IF NOT EXISTS digital_credentials;
USE digital_credentials;

-- Table for storing challenges
CREATE TABLE IF NOT EXISTS challenges (
    id VARCHAR(36) PRIMARY KEY,
    challenge VARCHAR(255) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_challenge (challenge),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing verification sessions
CREATE TABLE IF NOT EXISTS verification_sessions (
    id VARCHAR(36) PRIMARY KEY,
    challenge_id VARCHAR(36),
    status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending',
    presentation_data JSON,
    verified_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
    INDEX idx_challenge_id (challenge_id),
    INDEX idx_status (status)
);

-- Table for storing verified credentials data (optional)
CREATE TABLE IF NOT EXISTS verified_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_type VARCHAR(255),
    issuer VARCHAR(255),
    subject VARCHAR(255),
    claims JSON,
    verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE,
    INDEX idx_session_id (session_id),
    INDEX idx_credential_type (credential_type)
);
```

Khi cả hai tệp đã ở đúng vị trí, hãy mở terminal của bạn trong thư mục gốc của dự án và
chạy:

```bash
docker-compose up -d
```

Lệnh này sẽ khởi động một container MySQL trong nền.

### 4.2 Tổng quan về kiến trúc của ứng dụng Next.js

Ứng dụng Next.js của chúng ta được cấu trúc để tách biệt các mối quan tâm giữa frontend và
backend, mặc dù chúng là một phần của cùng một dự án.

- **Frontend (`src/app/page.tsx`):** Một trang [React](https://www.corbado.com/blog/react-passkeys) duy nhất khởi
  tạo luồng xác minh và hiển thị kết quả. Nó tương tác với API Digital Credential của
  trình duyệt.
- **Backend API Routes (`src/app/api/verify/...`):**
    - `start/route.ts`: Tạo yêu cầu OpenID4VP và một nonce
      [bảo mật](https://www.corbado.com/vi/blog/cach-bat-passkey-tren-android).
    - `finish/route.ts`: Nhận bản trình bày từ wallet (qua trình duyệt), xác thực nonce và
      giải mã thông tin xác thực.
- **Thư viện (`src/lib/`):**
    - `database.ts`: Quản lý tất cả các tương tác cơ sở dữ liệu (tạo challenge, xác minh
      phiên).
    - `crypto.ts`: Xử lý việc giải mã thông tin xác thực mDoc dựa trên CBOR.

Dưới đây là một sơ đồ minh họa kiến trúc nội bộ:

![Kiến trúc nội bộ NextJS](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_25_091202_f96ccb049f.svg)

### 4.3 Xây dựng Frontend

Frontend của chúng ta được cố ý làm nhẹ. Trách nhiệm chính của nó là hoạt động như một
trình kích hoạt hướng tới người dùng cho luồng xác minh và giao tiếp với cả backend của
chúng ta và khả năng xử lý thông tin xác thực gốc của trình duyệt. Nó không chứa bất kỳ
logic giao thức phức tạp nào; tất cả đều được ủy quyền.

Cụ thể, frontend sẽ xử lý những việc sau:

- **Tương tác người dùng:** Cung cấp một giao diện đơn giản, như nút "Xác minh", để người
  dùng bắt đầu quá trình.
- **Quản lý trạng thái:** Quản lý trạng thái giao diện người dùng, hiển thị chỉ báo tải
  trong khi quá trình xác minh đang diễn ra và hiển thị thông báo thành công hoặc lỗi cuối
  cùng.
- **Giao tiếp Backend (Yêu cầu):** Gọi `/api/verify/start` và nhận một payload JSON có cấu
  trúc (`protocol`, `request`, `state`) mô tả chính xác những gì wallet nên trình bày.
- **Gọi API trình duyệt:** Giao đối tượng JSON đó cho `navigator.credentials.get()`, đối
  tượng này sẽ hiển thị một mã QR gốc và chờ phản hồi của wallet.
- **Giao tiếp Backend (Phản hồi):** Khi API trình duyệt trả về Verifiable Presentation, nó
  sẽ gửi dữ liệu này đến endpoint `/api/verify/finish` của chúng ta trong một yêu cầu POST
  để xác thực cuối cùng phía máy chủ.
- **Hiển thị kết quả:** Cập nhật giao diện người dùng để thông báo cho người dùng biết
  liệu việc xác minh có thành công hay thất bại, dựa trên phản hồi từ backend.

Logic cốt lõi nằm trong hàm `startVerification`:

```typescript
// src/app/page.tsx

const startVerification = async () => {
    setLoading(true);
    setVerificationResult(null);

    try {
        // 1. Check if the browser supports the API
        if (!navigator.credentials?.get) {
            throw new Error("Browser does not support the Credential API.");
        }

        // 2. Ask our backend for a request object
        const res = await fetch("/api/verify/start");
        const { protocol, request } = await res.json();

        // 3. Hand that object to the browser – this triggers the native QR code
        const credential = await (navigator.credentials as any).get({
            mediation: "required",
            digital: {
                requests: [
                    {
                        protocol, // "openid4vp"
                        data: request, // contains dcql_query, nonce, etc.
                    },
                ],
            },
        });

        // 4. Forward the wallet response (from the browser) to our finish endpoint for server-side checks
        const verifyRes = await fetch("/api/verify/finish", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(credential),
        });

        const result = await verifyRes.json();

        if (verifyRes.ok && result.verified) {
            setVerificationResult(`Success: ${result.message}`);
        } else {
            throw new Error(result.message || "Verification failed.");
        }
    } catch (err) {
        setVerificationResult(`Error: ${(err as Error).message}`);
    } finally {
        setLoading(false);
    }
};
```

Hàm này cho thấy bốn bước chính của logic frontend: kiểm tra hỗ trợ API, lấy yêu cầu từ
backend, gọi API trình duyệt và gửi kết quả trở lại để xác minh. Phần còn lại của tệp là
boilerplate [React](https://www.corbado.com/blog/react-passkeys) tiêu chuẩn để quản lý trạng thái và hiển thị
giao diện người dùng, bạn có thể xem trong
[kho lưu trữ GitHub](https://github.com/corbado/digital-credentials-example).

#### Tại sao lại là `digital` và `mediation: 'required'`?

Bạn có thể nhận thấy lệnh gọi `navigator.credentials.get()` của chúng ta trông khác với
các ví dụ đơn giản hơn. Điều này là do chúng ta tuân thủ nghiêm ngặt
[đặc tả W3C Digital Credentials API](https://www.w3.org/TR/digital-credentials/#the-digital-credentials-api)
chính thức.

- **Thành viên `digital`:** Đặc tả yêu cầu tất cả các yêu cầu thông tin xác thực kỹ thuật
  số phải được lồng bên trong một đối tượng `digital`. Điều này cung cấp một không gian
  tên rõ ràng, được tiêu chuẩn hóa cho API này, phân biệt nó với các loại thông tin xác
  thực khác (như `password` hoặc `federated`) và cho phép các phần mở rộng trong tương lai
  mà không có xung đột.

- **`mediation: 'required'`:** Tùy chọn này là một tính năng
  [bảo mật](https://www.corbado.com/vi/blog/cach-bat-passkey-tren-android) và trải nghiệm người dùng quan trọng.
  Nó bắt buộc người dùng phải tương tác tích cực với một lời nhắc (ví dụ: quét
  [sinh trắc học](https://www.corbado.com/vi/blog/sinh-trac-hoc-nhan-thuc-nguoi-thanh-toan), nhập mã PIN hoặc màn
  hình đồng ý) để phê duyệt yêu cầu thông tin xác thực. Nếu không có nó, một trang web có
  thể cố gắng truy cập thông tin xác thực một cách âm thầm trong nền, điều này gây ra rủi
  ro riêng tư đáng kể. Bằng cách yêu cầu mediation, chúng ta đảm bảo rằng người dùng luôn
  kiểm soát và đưa ra sự đồng ý rõ ràng cho mỗi giao dịch.

### 4.4 Xây dựng các Endpoint Backend

Với giao diện người dùng React đã sẵn sàng, bây giờ chúng ta cần hai API route thực hiện
công việc nặng nhọc trên máy chủ:

1. **`/api/verify/start`** – xây dựng một yêu cầu OpenID4VP, lưu trữ một challenge sử dụng
   một lần trong MySQL và giao mọi thứ lại cho trình duyệt.
2. **`/api/verify/finish`** – nhận phản hồi từ wallet, xác thực challenge, xác minh & giải
   mã thông tin xác thực, và cuối cùng trả về một kết quả JSON ngắn gọn cho giao diện
   người dùng.

#### 4.4.1 `/api/verify/start`: Tạo yêu cầu OpenID4VP

```typescript
// src/app/api/verify/start/route.ts
import { NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { createChallenge, cleanupExpiredChallenges } from "@/lib/database";

export async function GET() {
    // 1️⃣  Create a short-lived, random nonce (challenge)
    const challenge = uuidv4();
    const challengeId = uuidv4();
    const expiresAt = new Date(Date.now() + 5 * 60 * 1000);

    await createChallenge(challengeId, challenge, expiresAt);
    cleanupExpiredChallenges().catch(console.error);

    // 2️⃣  Build a DCQL query that describes *what* we want
    const dcqlQuery = {
        credentials: [
            {
                id: "cred1",
                format: "mso_mdoc",
                meta: { doctype_value: "eu.europa.ec.eudi.pid.1" },
                claims: [
                    { path: ["eu.europa.ec.eudi.pid.1", "family_name"] },
                    { path: ["eu.europa.ec.eudi.pid.1", "given_name"] },
                    { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] },
                ],
            },
        ],
    };

    // 3️⃣  Return an object the browser can pass to navigator.credentials.get()
    return NextResponse.json({
        protocol: "openid4vp", // tells the browser which wallet protocol to use
        request: {
            dcql_query: dcqlQuery, // WHAT to present
            nonce: challenge, // anti-replay
            response_type: "vp_token",
            response_mode: "dc_api", // the wallet will POST directly to /finish
        },
        state: {
            credential_type: "mso_mdoc", // kept for later checks
            nonce: challenge,
            challenge_id: challengeId,
        },
    });
}
```

> **Các tham số chính**
>
> • **`nonce`** – thử thách mật mã liên kết yêu cầu & phản hồi (ngăn chặn tấn công phát
> lại). • **`dcql_query`** – Một đối tượng mô tả chính xác các claim chúng ta cần. Đối với
> guide này, chúng ta sử dụng cấu trúc `dcql_query` lấy cảm hứng từ các bản nháp gần đây
> của Digital Credential Query Language, mặc dù đây chưa phải là một tiêu chuẩn hoàn
> thiện. • **`state`** – JSON tùy ý được wallet phản hồi lại để chúng ta có thể tra cứu
> bản ghi trong DB.

#### 4.4.2 Các hàm trợ giúp cơ sở dữ liệu

Tệp `src/lib/database.ts` bao bọc các hoạt động MySQL cơ bản cho các challenge & phiên xác
minh (chèn, đọc, đánh dấu đã sử dụng). Việc giữ logic này trong một module duy nhất giúp
dễ dàng thay đổi kho dữ liệu sau này.

---

### 4.5 `/api/verify/finish`: Xác thực & Giải mã Presentation

```typescript
// src/app/api/verify/finish/route.ts
import { NextResponse, NextRequest } from "next/server";
import { v4 as uuidv4 } from "uuid";
import {
    getChallenge,
    markChallengeAsUsed,
    createVerificationSession,
    updateVerificationSession,
} from "@/lib/database";
import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto";

export async function POST(request: NextRequest) {
    const body = await request.json();

    // 1️⃣  Extract the verifiable presentation pieces
    const vpTokenMap = body.vp_token ?? body.data?.vp_token;
    const state = body.state;
    const mdocToken = vpTokenMap?.cred1; // we asked for this ID in dcqlQuery

    if (!vpTokenMap || !state || !mdocToken) {
        return NextResponse.json(
            { verified: false, message: "Malformed response" },
            { status: 400 },
        );
    }

    // 2️⃣  One-time-use challenge validation
    const stored = await getChallenge(state.nonce);
    if (!stored) {
        return NextResponse.json(
            { verified: false, message: "Invalid or expired challenge" },
            { status: 400 },
        );
    }

    const sessionId = uuidv4();
    await createVerificationSession(sessionId, stored.id);

    // 3️⃣  (Pseudo) cryptographic checks – replace with real mDL validation in prod
    // In a real application, you would use a dedicated library to perform full
    // cryptographic validation of the mdoc signature against the issuer's public key.
    const isValid = mdocToken.length > 0;
    if (!isValid) {
        await updateVerificationSession(sessionId, "failed", {
            reason: "mdoc validation failed",
        });
        return NextResponse.json(
            { verified: false, message: "Credential validation failed" },
            { status: 400 },
        );
    }

    // 4️⃣  Decode the mobile-DL (mdoc) payload into human readable JSON
    const decoded = await decodeDigitalCredential(mdocToken);
    const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"];

    await markChallengeAsUsed(state.nonce);
    await updateVerificationSession(sessionId, "verified", { readable });

    return NextResponse.json({
        verified: true,
        message: "mdoc credential verified successfully!",
        credentialData: readable,
        sessionId,
    });
}
```

> **Các trường quan trọng trong phản hồi của wallet**
>
> • **`vp_token`** – map chứa _mỗi_ thông tin xác thực mà wallet trả về. Đối với demo của
> chúng ta, chúng ta lấy `vp_token.cred1`. • **`state`** – bản sao của blob chúng ta đã
> cung cấp trong `/start`; chứa `nonce` để chúng ta có thể tra cứu bản ghi trong DB. •
> **`mdocToken`** – một cấu trúc CBOR được mã hóa Base64URL đại diện cho ISO mDoc.

### 4.6 Giải mã thông tin xác thực mdoc

Khi trình xác minh nhận được một thông tin xác thực mdoc từ trình duyệt, nó là một chuỗi
Base64URL chứa dữ liệu nhị phân được mã hóa CBOR. Để trích xuất các claim thực tế,
endpoint `finish` thực hiện một quy trình giải mã nhiều bước bằng cách sử dụng các hàm trợ
giúp từ `src/lib/crypto.ts`.

#### 4.6.1 Bước 1: Giải mã Base64URL và CBOR

Hàm `decodeDigitalCredential` xử lý việc chuyển đổi từ chuỗi đã mã hóa sang một đối tượng
có thể sử dụng được:

```typescript
// src/lib/crypto.ts
export async function decodeDigitalCredential(encodedCredential: string) {
    // 1. Convert Base64URL to standard Base64
    const base64UrlToBase64 = (input: string) => {
        let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
        const pad = base64.length % 4;
        if (pad) base64 += "=".repeat(4 - pad);
        return base64;
    };

    const base64 = base64UrlToBase64(encodedCredential);

    // 2. Decode Base64 to binary
    const binaryString = atob(base64);
    const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));

    // 3. Decode CBOR
    const decoded = await cbor.decodeFirst(byteArray);
    return decoded;
}
```

- **Base64URL sang Base64:** Chuyển đổi thông tin xác thực từ mã hóa Base64URL sang mã hóa
  Base64 tiêu chuẩn.
- **Base64 sang Nhị phân:** Giải mã chuỗi Base64 thành một mảng byte nhị phân.
- **Giải mã CBOR:** Sử dụng thư viện `cbor-web` để giải mã dữ liệu nhị phân thành một đối
  tượng JavaScript có cấu trúc.

#### 4.6.2 Bước 2: Trích xuất các Claim theo Namespace

Hàm `decodeAllNamespaces` xử lý thêm đối tượng CBOR đã giải mã để trích xuất các claim
thực tế từ các namespace liên quan:

```typescript
// src/lib/crypto.ts
export function decodeAllNamespaces(jsonObj) {
    const decoded = {};

    try {
        jsonObj.documents.forEach((doc, idx) => {
            // 1) issuerSigned.nameSpaces:
            const issuerNS = doc.issuerSigned?.nameSpaces || {};
            Object.entries(issuerNS).forEach(([nsName, entries]) => {
                if (!decoded[nsName]) decoded[nsName] = {};
                (entries as any[]).forEach((entry) => {
                    const bytes = Uint8Array.from(entry.value);
                    const decodedEntry = cbor.decodeFirstSync(bytes);
                    Object.assign(decoded[nsName], decodedEntry);
                });
            });

            // 2) deviceSigned.nameSpaces (if present):
            const deviceNS = doc.deviceSigned?.nameSpaces;
            if (deviceNS?.value?.data) {
                const bytes = Uint8Array.from(deviceNS.value);
                decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes);
            }
        });
    } catch (e) {
        console.error(e);
    }

    return decoded;
}
```

- **Lặp qua tất cả các tài liệu** trong thông tin xác thực đã giải mã.
- **Giải mã mỗi namespace** (ví dụ: `eu.europa.ec.eudi.pid.1`) để trích xuất các giá trị
  claim thực tế (như tên, ngày sinh, v.v.).
- **Xử lý cả các namespace do bên phát hành ký và do thiết bị ký** nếu có.

#### Ví dụ đầu ra

Sau khi chạy qua các bước này, endpoint finish thu được một đối tượng có thể đọc được chứa
các claim từ mdoc, ví dụ:

```json
{
    "family_name": "Doe",
    "given_name": "John",
    "birth_date": "1990-01-01"
}
```

Quá trình này đảm bảo rằng trình xác minh có thể trích xuất thông tin cần thiết từ thông
tin xác thực mdoc một cách an toàn và đáng tin cậy để hiển thị và xử lý thêm.

### 4.7 Hiển thị kết quả trên giao diện người dùng

Endpoint finish trả về một đối tượng JSON tối thiểu cho frontend:

```json
{
    "verified": true,
    "message": "mdoc credential verified successfully!",
    "credentialData": {
        "family_name": "Doe",
        "given_name": "John",
        "birth_date": "1990-01-01"
    }
}
```

Frontend nhận phản hồi này trong `startVerification()` và chỉ cần lưu nó vào trạng thái
React để chúng ta có thể hiển thị một thẻ xác nhận đẹp mắt hoặc hiển thị các claim riêng
lẻ – ví dụ: _“Chào mừng, John Doe (sinh ngày 1990-01-01)!”_.

## 5. Chạy Trình xác minh và các bước tiếp theo

Bây giờ bạn đã có một trình xác minh hoàn chỉnh, hoạt động, sử dụng khả năng xử lý thông
tin xác thực gốc của trình duyệt. Dưới đây là cách chạy nó cục bộ và những gì bạn có thể
làm để đưa nó từ một bằng chứng khái niệm (proof-of-concept) thành một ứng dụng sẵn sàng
cho môi trường sản xuất.

### 5.1 Cách chạy ví dụ

1. **Sao chép Kho lưu trữ:**

    ```bash
    git clone https://github.com/corbado/digital-credentials-example.git
    cd digital-credentials-example
    ```

2. **Cài đặt các phụ thuộc:**

    ```bash
    npm install
    ```

3. **Khởi động Cơ sở dữ liệu:** Đảm bảo Docker đang chạy trên máy của bạn, sau đó khởi
   động container MySQL:

    ```bash
    docker-compose up -d
    ```

4. **Chạy ứng dụng:**

    ```bash
    npm run dev
    ```

    Mở trình duyệt của bạn đến `http://localhost:3000`, và bạn sẽ thấy giao diện người
    dùng của trình xác minh. Bây giờ bạn có thể sử dụng CMWallet của mình để quét mã QR và
    hoàn thành luồng xác minh.

### 5.2 Các bước tiếp theo: Từ Demo đến Sản xuất

Hướng dẫn này cung cấp các khối xây dựng nền tảng cho một trình xác minh. Để làm cho nó
sẵn sàng cho môi trường sản xuất, bạn cần triển khai một số tính năng bổ sung:

- **Xác thực mật mã đầy đủ:** Triển khai hiện tại sử dụng một kiểm tra giữ chỗ
  (`mdocToken.length > 0`). Trong một kịch bản thực tế, bạn phải thực hiện xác thực mật mã
  đầy đủ của chữ ký mdoc đối với khóa công khai của bên phát hành (ví dụ: bằng cách giải
  quyết DID của họ hoặc lấy chứng chỉ khóa công khai của họ). Để biết các tiêu chuẩn giải
  quyết DID, hãy tham khảo
  [đặc tả W3C DID Resolution](https://www.w3.org/TR/did-resolution/).

- **Kiểm tra thu hồi của bên phát hành:** Thông tin xác thực có thể bị bên phát hành thu
  hồi trước ngày hết hạn. Một trình xác minh sản xuất phải kiểm tra trạng thái của thông
  tin xác thực bằng cách truy vấn danh sách thu hồi hoặc endpoint trạng thái do bên phát
  hành cung cấp.
  [Danh sách trạng thái Verifiable Credentials của W3C](https://www.w3.org/TR/vc-bitstring-status-list/)
  cung cấp tiêu chuẩn cho danh sách thu hồi thông tin xác thực.

- **Xử lý lỗi & Bảo mật mạnh mẽ:** Thêm xử lý lỗi toàn diện, xác thực đầu vào, giới hạn
  tốc độ trên các endpoint API và đảm bảo tất cả giao tiếp đều qua HTTPS (TLS) để bảo vệ
  dữ liệu khi truyền.
  [Hướng dẫn bảo mật API của OWASP](https://owasp.org/www-project-api-security/) cung cấp
  các phương pháp tốt nhất về bảo mật API toàn diện.

- **Hỗ trợ nhiều loại thông tin xác thực:** Mở rộng logic để xử lý các giá trị `doctype`
  và định dạng thông tin xác thực khác nhau nếu bạn dự kiến sẽ nhận được nhiều hơn chỉ
  thông tin xác thực PID của [Digital Identity](https://www.corbado.com/blog/digital-identity-guide) Châu Âu
  (EUDI).
  [Mô hình dữ liệu Verifiable Credentials của W3C](https://www.w3.org/TR/vc-data-model/)
  cung cấp các đặc tả định dạng VC toàn diện.

### 5.3 Những gì nằm ngoài phạm vi của Hướng dẫn này

Ví dụ này được cố ý tập trung vào luồng cốt lõi qua trung gian trình duyệt để làm cho nó
dễ hiểu. Các chủ đề sau được coi là nằm ngoài phạm vi:

- **Bảo mật sẵn sàng cho môi trường sản xuất:** Trình xác minh này dành cho mục đích giáo
  dục và thiếu sự củng cố cần thiết cho một môi trường trực tiếp.
- **W3C Verifiable Credentials:** Hướng dẫn này chỉ tập trung vào định dạng ISO mDoc cho
  giấy phép lái xe di động. Nó không đề cập đến các định dạng phổ biến khác như JWT-VCs
  hoặc VCs với Linked Data Proofs (LD-Proofs).
- **Các luồng OpenID4VP nâng cao:** Chúng ta không triển khai các tính năng OpenID4VP phức
  tạp hơn, chẳng hạn như giao tiếp trực tiếp từ wallet đến backend bằng `redirect_uri`
  hoặc đăng ký client động.

Bằng cách xây dựng trên nền tảng này và kết hợp các bước tiếp theo này, bạn có thể phát
triển một trình xác minh mạnh mẽ và an toàn, có khả năng tin cậy và xác thực thông tin xác
thực kỹ thuật số trong các ứng dụng của riêng bạn.

## Kết luận

Vậy là xong! Với ít hơn 250 dòng TypeScript, bây giờ chúng ta có một trình xác minh
end-to-end có thể:

1. Công bố một yêu cầu cho API thông tin xác thực của trình duyệt.
2. Cho phép bất kỳ wallet tuân thủ nào cung cấp một Verifiable Presentation.
3. Xác thực bản trình bày trên máy chủ.
4. Cập nhật giao diện người dùng trong thời gian thực.

Trong môi trường sản xuất, bạn sẽ thay thế việc xác thực giữ chỗ bằng các kiểm tra
[ISO 18013-5](https://www.corbado.com/glossary/iso-18013-5) đầy đủ, thêm tra cứu thu hồi của bên phát hành, giới
hạn tốc độ, ghi nhật ký kiểm toán, và tất nhiên là TLS end-to-end—nhưng các khối xây dựng
cốt lõi vẫn giữ nguyên.

## Tài liệu tham khảo

Dưới đây là một số tài liệu, đặc tả và công cụ chính được sử dụng hoặc tham chiếu trong
hướng dẫn này:

- **Kho lưu trữ dự án:**
    - [Mã nguồn hoàn chỉnh trên GitHub](https://github.com/corbado/digital-credentials-example)

- **Các đặc tả chính:**
    - [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/): Tiêu
      chuẩn nền tảng cho VCs.
    - [OpenID for Verifiable Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html):
      Giao thức trình bày được sử dụng để trao đổi thông tin xác thực.
    - [ISO/IEC 18013-5 (mDoc)](https://www.iso.org/standard/69084.html): Tiêu chuẩn quốc
      tế cho Giấy phép lái xe di động (mDLs).
    - [W3C Digital Credentials API](https://www.w3.org/TR/digital-credentials/#the-digital-credentials-api):
      API trình duyệt được sử dụng để yêu cầu thông tin xác thực từ một wallet.

- **Công cụ:**
    - [CMWallet cho Android](https://github.com/digitalcredentialsdev/CMWallet/actions/runs/16407676816/artifacts/3574255220):
      Wallet thử nghiệm được sử dụng trong guide này.

- **Thư viện:**
    - Next.js: Framework React để xây dựng frontend và backend.
    - [cbor-web](https://github.com/hildjj/cbor-web): Để giải mã thông tin xác thực mdoc
      được mã hóa bằng CBOR.
    - [mysql2](https://github.com/sidorares/node-mysql2): Client MySQL cho
      [Node.js](https://www.corbado.com/blog/nodejs-passkeys).
