Trong bài [Bitcoin và việc chứng thực tài liệu]({{< relref "bitcoin-chung-thuc-va-thua-hanh-vien.md" >}}), tôi tìm cách chứng minh một bài viết đã có từ **khi nào**. Bài này đi tiếp một câu hỏi khác: làm sao chứng minh bài viết là của **ai**?

Câu hỏi nghe đơn giản nhưng ngày càng khó. Ảnh chụp màn hình "bài của Trung Phan" có thể dựng trong vài giây. AI có thể viết một đoạn văn giống giọng tôi rồi gắn tên tôi vào. Một trang web giả giao diện blog này có thể nằm ở một tên miền na ná. Người đọc gần như không có cách tự kiểm tra.

Vì vậy tôi thử làm một việc nhỏ trên chính blog này: tạo cho mình một **định danh phi tập trung** (gọi tắt là DID) và dùng nó để **ký số** từng bài viết. Dưới đây là cách nó hoạt động, giải thích càng đơn giản càng tốt.

## 1. Hai cách để biết một bài viết là của ai

Hiện nay, hầu hết chúng ta chứng minh "đây là tôi" bằng **tài khoản do nền tảng cấp**: Facebook, LinkedIn, Gmail, X. Người đọc tin một bài là của tôi vì nền tảng hiển thị tên tôi bên cạnh.

Cách này tiện, nhưng mọi niềm tin dồn vào nền tảng:

- Tài khoản bị khóa, bị chiếm hay nền tảng đóng cửa thì "tôi" ở đó cũng mất.
- Bài viết chép sang nơi khác thì không còn gì chứng minh tác giả.
- Muốn kiểm tra, người đọc phải tin nền tảng, không tự kiểm được.

Có một cách khác, đã có từ lâu trong mật mã học: **chữ ký số**. Người viết tự ký vào bài, người đọc tự kiểm chữ ký. Không cần ai đứng giữa.

{{< diagram "did-web-dinh-danh-phi-tap-trung/hai-cach-tin" "Bên trái: người đọc phải tin nền tảng. Bên phải: chữ ký đi theo bài viết, người đọc tự kiểm." >}}

## 2. Chữ ký số giống mẫu chữ ký ở ngân hàng

Cách dễ hình dung nhất là nghĩ tới chữ ký tay ở ngân hàng.

- Khi mở tài khoản, bạn để lại **mẫu chữ ký**. Ngân hàng giữ mẫu này để đối chiếu.
- Mỗi lần bạn ký giấy tờ, nhân viên so chữ ký với mẫu. Khớp thì đúng là bạn.

Chữ ký số làm đúng như vậy, chỉ khác là dùng toán học thay cho mắt người:

- **Khóa riêng** là "cây bút" chỉ mình bạn giữ. Ai có nó là ký được thay bạn, nên nó phải được cất kỹ.
- **Khóa công khai** là "mẫu chữ ký" bạn đưa cho mọi người. Nhìn vào nó, không ai đoán ra được khóa riêng.
- Chữ ký tạo bằng khóa riêng chỉ khớp với đúng khóa công khai đi cặp với nó. Chừng nào khóa riêng còn được giữ kín thì không ai giả được chữ ký, và sửa nội dung dù chỉ một ký tự thì chữ ký không còn khớp.

{{< diagram "did-web-dinh-danh-phi-tap-trung/mau-chu-ky" "Mỗi bước ở ngân hàng có một bước tương ứng trong chữ ký số; khác ở chỗ máy kiểm bằng toán học và ai cũng tự kiểm được." >}}

Phần toán học này đã giải quyết xong từ lâu. Câu hỏi khó còn lại là: **treo "mẫu chữ ký" ở đâu** để người đọc tin rằng nó đúng là của tôi? Đó là việc của DID.

## 3. DID là gì?

DID (Decentralized Identifier, tức định danh phi tập trung) là một chuẩn quốc tế của W3C, tổ chức đặt chuẩn cho web. Chuẩn này chính thức được công bố ngày 19/7/2022.[^didcore]

Một DID là một cái tên, có dạng:

```text
did:<cách tra cứu>:<tên cụ thể>
```

Mỗi DID trỏ tới một **tài liệu DID**: một tệp nhỏ ghi các khóa công khai của chủ nhân và mỗi khóa dùng vào việc gì. Nói theo ví dụ ngân hàng, tài liệu DID chính là **tấm thẻ mẫu chữ ký**, chỉ khác là treo công khai cho mọi người xem.

Khác biệt quan trọng nhất so với tài khoản nền tảng: **không ai cấp DID cho tôi.** Tôi tự tạo cặp khóa trên máy mình và tự công bố tài liệu DID. Người giữ khóa riêng là người làm chủ định danh.

Phần "cách tra cứu" quyết định tấm thẻ được treo ở đâu. Có cách ghi lên blockchain, có cách dùng mạng riêng. Tôi chọn cách đơn giản nhất: treo ngay trên blog.

## 4. did:web: treo mẫu chữ ký trên chính blog

Định danh của tôi là:

```text
did:web:blog.phdtrung.com
```

Chữ `web` nghĩa là tài liệu DID nằm trên một website. Theo quy tắc của `did:web`, cái tên trên được đổi thành địa chỉ:[^didweb]

```text
https://blog.phdtrung.com/.well-known/did.json
```

Ai mở địa chỉ đó cũng thấy khóa công khai của tôi, tên là `#key-1`. Khóa này dùng thuật toán chữ ký Ed25519[^ed25519] và được ghi theo định dạng Multikey của W3C.[^cid] Khóa riêng đi cặp với nó thì chỉ nằm trên máy tính của tôi.

{{< diagram "did-web-dinh-danh-phi-tap-trung/did-hoat-dong" "Phần công khai nằm trên blog cho ai cũng tra được; phần bí mật chỉ nằm trên máy tôi. Hai khóa là một cặp." >}}

Tôi chọn `did:web` vì ba lý do:

1. **Không cần blockchain, không tốn phí.** Chỉ là một tệp nhỏ trên website tôi đã có.
2. **Dựa vào niềm tin sẵn có.** Bạn đọc vốn đã biết `blog.phdtrung.com` là blog của tôi. Treo mẫu chữ ký ở đây là tận dụng đúng niềm tin đó.
3. **Đã được dùng thật.** Ví dụ, AT Protocol, giao thức đứng sau mạng xã hội Bluesky, chấp nhận `did:web` làm định danh tài khoản.[^atproto]

## 5. Ký và kiểm một bài viết diễn ra thế nào?

**Khi ký**, việc của tôi chỉ là chạy một lệnh. Máy làm ba bước:

1. Lấy văn bản gốc của bài và tính **mã băm SHA-256**. Mã băm giống dấu vân tay của văn bản: mỗi văn bản cho một dấu vân tay riêng, đổi một dấu phẩy là dấu vân tay đổi hẳn.
2. Ghi một tờ khai ngắn: ai ký, bằng khóa nào, bài nào, dấu vân tay là gì, ký lúc nào.
3. Ký tờ khai đó bằng khóa riêng, đóng gói theo chuẩn JWS[^jws] với thuật toán EdDSA,[^eddsa] rồi công bố thành một tệp chữ ký trên blog.

**Khi kiểm**, người đọc làm ngược lại:

1. Tải tệp chữ ký. Trong đó ghi bài được ký bằng khóa `did:web:blog.phdtrung.com#key-1`.
2. Tra tài liệu DID trên blog để lấy khóa công khai `#key-1`.
3. Dùng khóa công khai kiểm chữ ký, rồi tính lại dấu vân tay của bài để so.

Khớp cả hai thì bài bạn đang đọc **đúng nguyên văn** bài mà người giữ khóa của blog đã ký.

{{< diagram "did-web-dinh-danh-phi-tap-trung/ky-va-kiem" "Tôi ký một lần; bất kỳ ai, bất cứ lúc nào cũng kiểm được mà không cần hỏi tôi." >}}

Bài nào đã ký sẽ có khối "Chữ ký số" ở cuối, kèm liên kết tới tệp chữ ký và tài liệu DID. Chính bài viết này cũng được ký. Ai biết dùng dòng lệnh có thể kiểm bằng công cụ của blog hoặc bất kỳ thư viện nào hỗ trợ chuẩn JWS. Không cần tài khoản, không cần hỏi tôi, không cần tin GitLab hay Cloudflare.

## 6. Chuyện thật trên blog này: khóa cất ở đâu, ký thế nào?

Nghe "khóa riêng", "ký số" thì dễ tưởng là thứ gì phức tạp lắm. Thực tế trên blog này đơn giản hơn nhiều.

### Khóa riêng: một tệp nhỏ trên máy Mac của tôi

Khóa riêng của tôi là **một tệp văn bản chỉ 3 dòng, nặng 119 byte**, nằm trong một thư mục ẩn trên chiếc Mac tôi dùng hằng ngày. Không có thẻ cứng, không có thiết bị đặc biệt nào.

Tệp này được bảo vệ bằng ba lớp:

- **Quyền đọc:** chỉ tài khoản máy tính của tôi mở được tệp. Tài khoản khác trên cùng máy cũng không đọc được.
- **Mã hóa ổ đĩa:** ổ đĩa của máy được mã hóa bằng FileVault. Máy bị mất cắp thì người lấy cũng không đọc được tệp nếu không có mật khẩu đăng nhập.
- **Không bao giờ rời máy:** tệp nằm ngoài thư mục mã nguồn của blog, nên không bị đưa lên GitLab, không gửi qua email hay chat, không đưa cho AI.

Điểm yếu cũng cần nói rõ: mất tệp này (hỏng máy, xóa nhầm) là mất khả năng ký. Vì vậy khóa cần một bản sao lưu cất riêng, có mã hóa, ví dụ trên USB mã hóa hoặc trong trình quản lý mật khẩu. Còn nếu ai đó lấy được tệp, tôi phải đổi khóa ngay: treo khóa công khai mới lên blog và gỡ khóa cũ.

### Mẫu chữ ký: một tệp công khai trên blog

Phần công khai thì ai cũng xem được tại `blog.phdtrung.com/.well-known/did.json`. Bỏ bớt các dòng kỹ thuật, nó trông như sau:

```json
{
  "id": "did:web:blog.phdtrung.com",
  "verificationMethod": [{
    "id": "did:web:blog.phdtrung.com#key-1",
    "type": "Multikey",
    "publicKeyMultibase": "z6MkfqVrcqW8qHLSEfUKW64nowR6Jbi99bHfGGj4WChaZy6o"
  }]
}
```

Chuỗi bắt đầu bằng `z6Mk…` chính là khóa công khai, tức "mẫu chữ ký". Từ chuỗi này không ai suy ngược ra được tệp khóa riêng trên máy tôi.

### Ký một bài: gõ một lệnh

Khi một bài đã viết xong và tôi đã đọc duyệt lần cuối, tôi mở cửa sổ dòng lệnh trên Mac và gõ:

```bash
python3 scripts/did/sign_post.py content/posts/did-web-dinh-danh-phi-tap-trung.md
```

Trong vài giây, công cụ ký làm lần lượt:

1. Kiểm tra tệp khóa vẫn đang được khóa quyền đọc như trên; nếu không thì dừng.
2. Đọc khóa riêng, suy ra khóa công khai và so với "mẫu chữ ký" đang treo trên blog. Nếu khác (ví dụ lấy nhầm khóa) thì dừng, không ký.
3. Tính dấu vân tay của bài, ghi tờ khai, ký tờ khai.
4. Ghi kết quả ra một tệp chữ ký nằm cạnh bài viết.

Màn hình chỉ hiện ba dòng: địa chỉ bài, dấu vân tay và nơi lưu tệp chữ ký. Khóa riêng không bao giờ được in ra.

Ví dụ, tờ khai trong tệp chữ ký của lần ký đầu tiên bài này trông như sau (dấu vân tay rút gọn):

```json
{
  "did": "did:web:blog.phdtrung.com",
  "kid": "did:web:blog.phdtrung.com#key-1",
  "url": "https://blog.phdtrung.com/posts/did-web-dinh-danh-phi-tap-trung/",
  "sha256": "6df08371…dae0b78",
  "signedAt": "2026-10-22T12:54:02+07:00"
}
```

Kèm theo là chữ ký: một chuỗi khoảng 550 ký tự, chỉ khóa riêng của tôi tạo ra được.

### Từ máy tôi lên blog

Tệp chữ ký đi lên blog theo đúng quy trình xuất bản thường ngày: lưu vào kho mã nguồn trên GitLab, mở yêu cầu hợp nhất (Merge Request), tôi duyệt, rồi hệ thống tự dựng lại trang. Trước khi đưa lên, hệ thống kiểm tra tự động còn so nội dung bài với dấu vân tay đã ký. Nếu ai sửa bài sau khi tôi ký, dù chỉ một chữ, bước kiểm tra sẽ chặn lại cho tới khi tôi ký lại.

Đoạn mô phỏng dưới đây tóm cả quá trình: ký trên máy tôi, kiểm trên trình duyệt của người đọc, và điều gì xảy ra khi có người sửa bài.

{{< diagram "did-web-dinh-danh-phi-tap-trung/mo-phong-ky-kiem" "Mô phỏng, lặp lại mỗi 14 giây: ký bằng một lệnh, công bố tệp chữ ký, người đọc tự kiểm; sửa một chữ là chữ ký không còn khớp." >}}

### Ranh giới với AI

Blog này được vận hành cùng AI, nên tôi viết thành quy tắc bắt buộc cho mọi trợ lý AI làm việc với blog: **không in, không đọc, không lưu khóa riêng; không tự chạy lệnh ký.** AI được giúp viết bài, viết công cụ ký và công cụ kiểm, chuẩn bị mọi thứ, nhưng bước ký cuối cùng luôn do tôi tự gõ lệnh. Chữ ký có giá trị chính vì chỉ tôi tạo ra được nó.

## 7. Chữ ký chứng minh được gì, không chứng minh được gì?

Công nghệ mới rất dễ bị hiểu quá lên, nên phần này tôi nói thẳng.

**Chữ ký chứng minh được:**

- Bài được ký bằng khóa đang treo trên `blog.phdtrung.com`.
- Bài **không bị sửa** sau khi ký.
- Bản sao của bài ở nơi khác, nếu kèm tệp chữ ký, thì người đọc tự kiểm được có đúng nguyên văn hay không.

**Chữ ký không chứng minh được:**

- **Thời điểm viết.** Giờ ký do máy tôi tự ghi. Muốn chứng minh "đã có từ khi nào" thì cần một nguồn độc lập, như dấu thời gian OpenTimestamps trên Bitcoin trong bài trước.
- **Nội dung là đúng.** Chữ ký chỉ nói "tôi viết đoạn này và chịu trách nhiệm về nó", không nói đoạn này đúng.
- **Tôi là ai ngoài đời.** `did:web` chỉ chứng minh quyền kiểm soát một tên miền và một khóa, không gắn với căn cước công dân của tôi.

**Điểm yếu của `did:web`:**

- **Chỉ an toàn bằng tên miền.** Ai chiếm được tên miền hay quyền sửa blog thì có thể thay "mẫu chữ ký". Chính đặc tả `did:web` thừa nhận nhiều hướng tấn công đến từ hệ thống tên miền (DNS).[^didweb]
- **Không lưu lịch sử.** Tệp `did.json` chỉ có phiên bản hiện tại. Khi đổi khóa, muốn kiểm các chữ ký cũ thì phải giữ khóa cũ trong tệp một thời gian, hoặc tra lịch sử của kho mã nguồn.
- **Chưa phải chuẩn W3C chính thức.** `did:web` do một nhóm cộng đồng của W3C soạn. Chuẩn DID nói chung thì đã chính thức.

**Về pháp lý:** chữ ký này **không phải** chữ ký số theo nghĩa của pháp luật Việt Nam. Luật Giao dịch điện tử yêu cầu chữ ký số phải được bảo đảm bởi chứng thư chữ ký số; với chữ ký số công cộng, chứng thư đó do tổ chức cung cấp dịch vụ chứng thực chữ ký số công cộng cấp.[^luat20] Chữ ký trên blog chỉ là công cụ kỹ thuật để người đọc tự kiểm, không dùng thay chữ ký số trong hợp đồng, giao dịch hay hồ sơ hành chính. (Nhận định này chỉ để tham khảo, không phải tư vấn pháp lý.)

## 8. Bước tiếp theo: từ định danh đến chứng nhận

Định danh và chữ ký bài viết mới là bước đầu. Bước tiếp theo trong cùng họ chuẩn là **chứng nhận có thể kiểm chứng** (Verifiable Credentials), được W3C công bố chính thức ngày 15/5/2025.[^vc2]

Ví dụ, với định danh này tôi có thể cấp:

- Giấy xác nhận một người đã tham gia buổi chia sẻ hay khóa học do tôi hướng dẫn.
- Thư giới thiệu mà người nhận tự mang đi trình, bên nhận tự kiểm chứng mà không cần gọi hỏi tôi.

Tôi chưa làm phần này. Trước khi cấp chứng nhận cho người khác, tôi muốn tự dùng định danh và cách giữ khóa của mình một thời gian, xem nó có đứng vững không.

## Thay lời kết

Thử nghiệm này không giải quyết bài toán danh tính trên Internet. Nó chỉ chuyển một phần niềm tin từ "tin vào nơi bạn thấy bài" sang "tự kiểm được bài có đúng nguyên văn hay không".

Ghép với dấu thời gian trên Bitcoin, một bài viết có thể mang theo hai bằng chứng độc lập: **ai viết** và **viết từ khi nào**. Cả hai đều kiểm được mà không cần hỏi tôi, cũng không cần tin bất kỳ nền tảng nào.

Khi nội dung giả rẻ hơn bao giờ hết, tôi nghĩ người viết nên bắt đầu ký tên mình theo cách mà máy tính cũng kiểm được.

[^didcore]: W3C, [Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-1.0/), W3C Recommendation ngày 19/7/2022.
[^didweb]: W3C Credentials Community Group, [did:web Method Specification](https://w3c-ccg.github.io/did-method-web/): định danh không có đường dẫn được đổi thành `https://<tên miền>/.well-known/did.json`; mục bảo mật nêu rủi ro DNS và việc đặc tả không quy định cơ chế xác thực khi ghi tài liệu DID.
[^ed25519]: Thuật toán chữ ký Ed25519 được chuẩn hóa trong [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) (IETF, tháng 1/2017).
[^cid]: W3C, [Controlled Identifiers v1.0](https://www.w3.org/TR/cid-1.0/), W3C Recommendation ngày 15/5/2025, định nghĩa kiểu khóa `Multikey` và trường `publicKeyMultibase`.
[^atproto]: AT Protocol, [DID specification](https://atproto.com/specs/did): giao thức hỗ trợ hai phương thức `did:plc` và `did:web`.
[^jws]: JSON Web Signature (JWS) được chuẩn hóa trong [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515) (IETF, tháng 5/2015).
[^eddsa]: Thuật toán `EdDSA` cho JOSE/JWS được định nghĩa trong [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) (IETF, tháng 1/2017).
[^luat20]: [Luật Giao dịch điện tử số 20/2023/QH15](https://vanban.chinhphu.vn/?pageid=27160&docid=208421), hiệu lực từ 01/7/2024. Điều 22 khoản 3: chữ ký số phải được bảo đảm bởi chứng thư chữ ký số; chữ ký số công cộng phải được bảo đảm bởi chứng thư chữ ký số của tổ chức cung cấp dịch vụ chứng thực chữ ký số công cộng.
[^vc2]: W3C, [Verifiable Credentials Data Model v2.0](https://www.w3.org/TR/vc-data-model-2.0/), W3C Recommendation ngày 15/5/2025.
