1.1. Không có "tài liệu". Chỉ có byte.
Khi anh mở một file PDF, anh thấy một quyết định hành chính: có quốc hiệu, có số văn bản, có chữ ký, có con dấu đỏ.
Máy tính không thấy gì trong số đó. Nó thấy một dãy số nguyên từ 0 đến 255, xếp nối đuôi nhau:
37 80 68 70 45 49 46 55 10 37 226 227 207 211 10 ...Chấm hết. Không có "trang", không có "đoạn văn", không có "chữ ký". Chỉ là 1.769 con số xếp hàng — đó là kích thước file PDF mẫu trong lab của ta.
Điều này nghe hiển nhiên đến mức nhàm chán. Nhưng nó là nguồn gốc của ba hệ quả mà nếu không nắm, anh sẽ gặp lỗi mà không hiểu tại sao. Ta đi từng cái.
1.2. Byte không mang ý nghĩa. Ý nghĩa do trình diễn giải gán vào.
Lấy hai byte: 0x30 0x82.
Chúng có nghĩa gì? Câu trả lời phụ thuộc hoàn toàn vào ai đang đọc:
| Trình diễn giải | Hiểu 0x30 0x82 là | |
|---|---|---|
| Bộ đọc ASCII | ký tự '0' rồi một byte không in được | |
| Bộ phân tích DER | mở đầu một SEQUENCE, độ dài ghi ở dạng dài 2 byte | |
| Số nguyên 16-bit big-endian | 12418 | |
| Bộ giải mã UTF-8 | '0' rồi một byte lỗi |
Cùng một cặp byte, bốn ý nghĩa hoàn toàn khác nhau. Byte tự nó trống rỗng về ngữ nghĩa.
Đây không phải triết lý. Nó dẫn thẳng tới hệ quả đầu tiên và quan trọng nhất.
1.3. Hệ quả 1 — Chữ ký số bảo vệ BYTE, không bảo vệ Ý NGHĨA
Hàm băm SHA-256 chỉ ăn byte. Nó không biết PDF là gì, không biết tiếng Việt, không biết đâu là số văn bản. Nó nhận một dãy số và nhả ra 32 byte.
Cho nên phát biểu chính xác về điều mà chữ ký số bảo đảm là:
"Dãy byte này không thay đổi kể từ khi được ký."
Chứ không phải:
"Văn bản này vẫn mang nội dung như lúc được ký."
Khoảng cách giữa hai câu đó có tên: khoảng cách ngữ nghĩa (semantic gap). Và trong khoảng cách ấy trú ngụ toàn bộ các lớp tấn công tinh vi nhất vào chữ ký số.
Cho anh một ví dụ để thấy nó cụ thể ra sao. Giả sử một PDF chứa đoạn mã điều kiện hiển thị:
Nếu hôm nay trước ngày 01/01/2027 → hiện "Số tiền: 10.000.000 đồng"
Ngược lại → hiện "Số tiền: 10.000.000.000 đồng"Dãy byte không hề đổi. Hash không đổi. Chữ ký verify hợp lệ vĩnh viễn. Nhưng người ký năm 2026 nhìn thấy một con số, còn tòa án năm 2028 nhìn thấy con số khác.
Đây chính là cơ sở của họ Shadow Attack mà ta sẽ mổ kỹ ở Bài 18. Bây giờ anh chỉ cần khắc cốt một điều: hợp lệ về mật mã không đồng nghĩa với đáng tin về nội dung. Chữ ký số bảo vệ tính toàn vẹn của byte — phần diễn giải byte thành nội dung là trách nhiệm của tầng khác.
1.4. Hệ quả 2 — Cùng một dãy byte có nhiều cách viết ra
Byte là số nhị phân, con người không đọc được trực tiếp. Nên ta luôn phải mã hóa lại để nhìn, chép, dán vào JSON. Và mỗi cách mã hóa cho ra một chuỗi ký tự khác nhau, dù dãy byte gốc y hệt.
Lấy ba byte đầu của một chứng thư X.509 thật, trích từ lab:
Byte gốc (nhị phân) : 00110000 10000010 00000100
Byte gốc (hex) : 30 82 04
Base64 : MIIE...Ta thử lần theo phép biến đổi base64 để anh thấy nó không phải phép màu. Base64 gom bit thành nhóm 6, mỗi nhóm tra bảng A–Z a–z 0–9 + /:
00110000 10000010 00000100
└────┘└─ ──┘└── ─┘└────┘
001100 = 12 → 'M'
001000 = 8 → 'I'
001000 = 8 → 'I'
000100 = 4 → 'E'Ra MIIE. Bây giờ anh hiểu vì sao mọi chứng thư X.509 mã base64 đều bắt đầu bằng MII: vì mọi chứng thư DER đều mở đầu bằng 0x30 0x82 (một SEQUENCE dài từ 256 đến 65535 byte). Đây không phải mẹo vặt học thuộc — nó là hệ quả trực tiếp của cấu trúc.
Giờ so với việc mã hóa nhầm. Nếu anh lấy file PEM — tức là bản DER đã bọc base64 kèm dòng -----BEGIN CERTIFICATE----- — rồi đem base64 thêm lần nữa:
Ba dấu gạch đầu "---" = 0x2D 0x2D 0x2D
00101101 00101101 00101101
001011 = 11 → 'L'
010010 = 18 → 'S'
110100 = 52 → '0'
101101 = 45 → 't'Ra LS0t.... Đây chính là chuỗi mà lab in ra khi mô phỏng lỗi:
ĐÚNG - base64(DER): MIIENjCCAp6gAwIBAgIQVAT//rcDP7MW1nIgHBe5vzANBgkqhk...
SAI - base64(PEM): LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVOakNDQX...Đối chiếu với đặc tả: bảng 2-5 và 2-6 quy định cert_data, ca_cert, root_cert đều là "dạng base64". Nhưng base64 của cái gì? Đặc tả không nói rõ, và đây là một trong những chỗ mơ hồ dẫn tới lỗi tích hợp thật.
Câu trả lời đúng là base64 của DER. Và anh có ngay công cụ kiểm tra bằng mắt, không cần chạy code: chuỗi bắt đầu bằng MII là đúng, bắt đầu bằng LS0 là đã encode hai lần.
1.5. Hệ quả 3 — Cùng một "nội dung" lại có nhiều dãy byte
Đây là chiều ngược lại của hệ quả 2, và nó tinh vi hơn nhiều.
Xét hai đoạn XML:
<hoadon xmlns="urn:vn" so="001" ngay="2026-07-22"/><hoadon ngay="2026-07-22" so="001" xmlns="urn:vn" />Với mọi bộ phân tích XML trên đời, hai đoạn này giống hệt nhau về mặt logic: cùng phần tử, cùng thuộc tính, cùng giá trị. Thứ tự thuộc tính trong XML là không có ý nghĩa, theo đúng đặc tả W3C.
Nhưng dãy byte thì khác nhau. Nên hash khác nhau. Nên chữ ký ký trên bản này sẽ không verify được trên bản kia.
Bây giờ hình dung tình huống thực tế với hóa đơn điện tử. Hệ thống A ký hóa đơn. Hệ thống B nhận, nạp vào bộ nhớ, xử lý, rồi ghi ra file. Bộ thư viện XML của B sắp xếp lại thuộc tính theo thứ tự alphabet — hoàn toàn hợp lệ theo chuẩn. Hóa đơn về mặt nội dung không đổi một ly. Nhưng chữ ký chết.
Đây là lý do XML-DSig phải có bước chuẩn hóa (canonicalization, viết tắt C14N): đưa XML về một dạng byte duy nhất theo quy tắc cố định trước khi băm. Ta sẽ mổ kỹ ở Bài 19, kể cả cái bẫy giữa c14n và c14n-excl mà hóa đơn điện tử Việt Nam hay vấp.
Và ngược lại — bây giờ anh hiểu vì sao PDF không cần chuẩn hóa: vì PAdES băm trực tiếp trên byte thô của file, không qua bất kỳ tầng diễn giải nào. Đơn giản hơn, nhưng đổi lại phải giải bài toán /ByteRange khá xoắn não ở Bài 17.
Ba định dạng, ba triết lý khác nhau, đều bắt nguồn từ chính hệ quả này:
| Băm cái gì | Cần chuẩn hóa? | |
|---|---|---|
| PDF (PAdES) | Byte thô của file | Không |
| XML (XAdES) | Byte sau khi chạy C14N | Bắt buộc |
| JSON (JAdES) | Byte của payload đã base64url-hóa | Tránh né bằng cách đóng băng chuỗi |
1.6. Ba hệ quả, gộp lại thành một nguyên lý
Ta rút gọn cả bài về một câu, và tôi muốn anh mang câu này theo suốt các bài sau:
Chữ ký số vận hành ở tầng byte. Mọi thứ nằm phía trên tầng byte — ý nghĩa, cách hiển thị, cấu trúc logic — đều nằm ngoài phạm vi bảo vệ của nó.
Từ nguyên lý đó chảy ra:
- Xuống dưới — hàm băm ăn byte, nên tài liệu 5 MB hay 32 byte hash đều là byte. Đây là điều kiện cho phép ký từ xa tồn tại. Ta đã chạm vào ở phần trước, sẽ dựng lại đầy đủ ở Bài 4.
- Sang ngang — cùng nội dung mà khác byte thì hash khác, nên XML phải chuẩn hóa. Bài 19.
- Lên trên — cùng byte mà khác cách hiển thị thì người dùng thấy khác nhau, nên có Shadow Attack. Bài 18.
Ba hướng, một gốc.
1.7. Tự kiểm tra
Trước khi sang Bài 2, anh thử tự trả lời bốn câu này. Nếu trả lời trôi chảy thì nền móng đã chắc:
- Một chuỗi base64 bắt đầu bằng
LS0tLS1CRUdJTi— nó chứa gì, và vì sao ta biết ngay mà không cần giải mã? - Vì sao hai file XML có nội dung logic y hệt nhau lại cho ra hai chữ ký khác nhau?
- Một PDF verify ra
valid = True. Kết luận nào sau đây là đúng: (a) nội dung chưa bị sửa, hay (b) dãy byte chưa bị sửa? Khác biệt nằm ở đâu? - Vì sao mọi chứng thư X.509 mã base64 đều mở đầu bằng
MII?