Bạn tải một file cài đặt về. Bên cạnh nó, trang web ghi một dòng checksum SHA256 dài loằng ngoằng và bảo "kiểm tra để chắc file không hỏng". Bạn chạy sha256sum, thấy khớp, yên tâm cài. Nhưng hãy nghĩ kỹ: cái checksum đó cũng in trên chính trang web ấy — nếu ai đó đủ sức tráo file độc lên trang, họ cũng đủ sức sửa luôn dòng checksum cho khớp. Nó giống như một lá thư niêm phong bằng băng dính trong suốt: ai cũng bóc ra, sửa nội dung, rồi dán lại y hệt. Cái thật sự chống giả mạo phải là con dấu sáp có khuôn riêng — thứ mà kẻ khác không làm nhái được vì họ không có cái khuôn. Bài này đo đúng khác biệt đó: giữa checksum (băng dính) và chữ ký số (con dấu sáp).

Chủ đề là ký và xác minh tạo tác (artifact): quy trình nhà phát hành gắn một chữ ký số vào bản phát hành, để người dùng kiểm rằng file đúng là của họ và chưa bị đụng vào. Tôi dựng toàn bộ quy trình trong container dùng một lần bằng Ed25519, thử ba cách tấn công, rồi so với checksum. Đo 3 lần, tự dọn. Không nhắm vào hệ thống của ai khác.

Quy trình ký, đo bằng ba cách tấn công

Nhà phát hành sinh một cặp khoá: khoá riêng giữ kín, khoá công công bố rộng rãi. Họ ký artifact bằng khoá riêng, tạo ra một chữ ký 64 byte. Người dùng có khoá công thì kiểm được chữ ký đó. Tôi dựng đúng như vậy rồi tấn công.

Cách 1 — người dùng thật kiểm bản gốc:

verify artifact.bin bang khoa cong -> Signature Verified Successfully

Bản gốc, chữ ký gốc, khoá công thật: hợp lệ. Đây là đường đi bình thường. Cách 2 — kẻ tấn công sửa file, giữ nguyên chữ ký cũ:

sua 1 byte -> Signature Verification Failure

Chỉ đổi một byte đầu file, để nguyên chữ ký cũ, và xác minh thất bại ngay. Đây là điểm cốt lõi: chữ ký gắn chặt với đúng từng byte của nội dung. Kẻ tấn công chèn mã độc vào file cài đặt thì chữ ký cũ không còn khớp, người dùng biết ngay khi verify. Cách 3 — kẻ tấn công tự ký bằng khoá riêng của chính hắn:

ke tu ky bang khoa cua han, verify bang khoa cong that -> Signature Verification Failure

Kẻ tấn công thông minh hơn: hắn sửa file rồi tự ký lại bằng khoá riêng của hắn, để chữ ký "khớp" với file đã sửa. Nhưng khi người dùng kiểm bằng khoá công thật của nhà phát hành, nó vẫn thất bại — vì chữ ký của hắn chỉ khớp với khoá công của hắn, không khớp khoá công thật. Đây là chỗ chữ ký chứng minh danh tính: không chỉ "file không đổi" mà "file được ký bởi đúng người giữ khoá riêng kia". Cả ba kết quả giống hệt qua 3 lần đo. Điều kiện để kẻ tấn công qua được cửa này chỉ có một: lấy được khoá riêng. Không có khoá riêng, hắn không thể tạo một chữ ký khớp khoá công thật — dù hắn kiểm soát hoàn toàn file và cả trang tải về.

Tương phản: checksum chứng minh ít hơn bạn tưởng

Giờ quay lại cái checksum SHA256 ở đầu bài. Tôi tính checksum bản gốc và bản đã sửa:

checksum ban goc:  a8e13e28fddbed7d...636fa0b7
checksum ban sua:  28a357a8cfb86d7f...06953ec6

Đúng là chúng khác nhau — nên nếu file hỏng do đường truyền, checksum sẽ lệch và bạn biết. Checksum làm tốt việc đó: bắt hỏng ngẫu nhiên. Nhưng nó có một lỗ hổng chí mạng trước kẻ tấn công chủ động: kẻ nào sửa được file thì cũng tính lại checksum mới và thay luôn dòng checksum trên trang. Người dùng tải file độc, tính SHA256, so với dòng checksum (cũng đã bị thay) — và thấy khớp. Checksum không đòi bí mật gì cả; sha256sum ai chạy cũng ra, kể cả kẻ tấn công.

Đó là khác biệt cốt lõi giữa hai thứ trông na ná nhau. Checksum chứng minh toàn vẹn — file này khớp với chuỗi băm này; nhưng vì ai cũng tính được, nó không nói gì về việc ai tạo ra chuỗi băm đó. Chữ ký chứng minh toàn vẹn VÀ danh tính — file này chưa đổi, và được ký bởi người giữ khoá riêng; vì tạo chữ ký cần bí mật (khoá riêng), kẻ tấn công không làm giả được. Nói ngắn: checksum chống tai nạn, chữ ký chống đối thủ. Một trang phát hành nghiêm túc thường có cả hai — checksum để bắt lỗi tải, và chữ ký (hoặc checksum được ký) để chống giả mạo. Nếu chỉ có checksum trần, bạn đang tin vào chính cái trang mà kẻ tấn công cần chiếm.

Nhưng khoá công đó, bạn tin từ đâu?

Có một mắt xích mà phép đo trên giả định sẵn: người dùng đã có khoá công thật của nhà phát hành. Cả sự bảo đảm nằm ở đó. Nếu kẻ tấn công lừa được bạn dùng khoá công của hắn ngay từ đầu, thì mọi thứ hắn ký sẽ "verify thành công" — vì bạn đang kiểm bằng đúng khoá của hắn. Chữ ký chỉ mạnh bằng độ tin của khoá công bạn dùng để kiểm.

Đây là bài toán phân phối khoá, và nó không có lời giải miễn phí. Vài cách thực tế: khoá công đi kèm hệ điều hành hoặc trình quản lý gói (như khoá GPG của kho apt, được cài sẵn lúc bạn cài máy — tin một lần lúc đầu); khoá công bố qua nhiều kênh độc lập để kẻ tấn công khó chiếm hết; hoặc mô hình mới hơn như Sigstore, ký "không khoá" (keyless) gắn danh tính vào tài khoản định danh và ghi mọi chữ ký vào một sổ công khai minh bạch (transparency log) để ai cũng soi được. Điểm chung: bạn phải neo lòng tin vào một chỗ nào đó khó giả mạo hơn chính file tải về. Không neo được thì chữ ký chỉ dời vấn đề từ "tin file" sang "tin khoá", chứ không xoá được nó.

Ba điều rút ra: (1) chữ ký gắn với từng byte — sửa một byte là chữ ký gãy, toàn vẹn được bảo đảm bằng toán học chứ không bằng thiện chí; (2) chữ ký gắn với danh tính — kẻ tấn công tự ký bằng khoá của hắn vẫn trượt khi kiểm bằng khoá công thật, điều duy nhất phá được là để lộ khoá riêng, nên bảo vệ khoá riêng chính là bảo vệ cả chuỗi; (3) checksum không thay được chữ ký — nó bắt hỏng ngẫu nhiên, không chống được kẻ kiểm soát nơi phát hành, đừng nhầm "khớp checksum" với "an toàn".

Mẫu số chung

Cái lằn ranh ở đây — toàn vẹn (khớp một giá trị ai cũng tính được) so với xác thực (chứng minh danh tính bằng một thứ chỉ chủ nhân tạo được) — không riêng gì file tải về; nó là khác biệt nền tảng giữa "chống tai nạn" và "chống đối thủ" ở mọi tầng.

Trong thế giới Java, đây đúng là lý do Maven Central bắt buộc mọi artifact phải ký GPG, không chỉ kèm checksum: jarsigner và chữ ký GPG chứng minh gói đến từ đúng nhà phát hành, còn checksum .sha1 bên cạnh chỉ bắt lỗi tải. Cùng đó, đừng nhầm hashCode() hay một MD5 kiểm cache với bảo mật — chúng là toàn vẹn, không phải xác thực.

Trong Go, ví dụ đẹp nhất là sum.golang.org — checksum database của Go module là một transparency log công khai, nghĩa là Go không chỉ băm module mà còn neo cái băm đó vào một sổ chỉ-thêm mà ai cũng soi được, đúng mô hình Sigstore ở trên; đó là câu trả lời của Go cho "làm sao tin cái băm này không bị tráo".

Còn ở cơ sở dữ liệu, cùng lằn ranh: một backup kèm checksum bắt được hỏng đĩa, nhưng muốn chống sửa cố ý thì cần backup được ký (hay HMAC bằng khoá riêng); và một giá trị lưu kèm HMAC chống được người sửa thẳng vào bảng, còn một CRC/checksum thì không, vì kẻ sửa dữ liệu tính lại CRC dễ như bạn.

Mẫu số chung: một chuỗi băm ai cũng tính lại được chỉ chứng minh toàn vẹn (chống hỏng ngẫu nhiên); muốn chống một đối thủ chủ động — kẻ vừa sửa được dữ liệu vừa sửa được cái đi kèm — thì thứ đi kèm phải cần một bí mật để tạo ra, và bí mật đó là cái kẻ tấn công không có. Vì thế câu hỏi đúng khi thấy "một chuỗi băm để kiểm" không phải "nó có khớp không" mà là "tạo ra chuỗi này có cần bí mật gì không — nếu không, thì kẻ sửa được dữ liệu cũng tạo lại được nó, và tôi chẳng được bảo vệ trước ai cả".

Muốn tự tay chứng minh, ký một file bằng Ed25519 rồi thử phá trong ba mươi giây:

openssl genpkey -algorithm ed25519 -out priv.pem
openssl pkey -in priv.pem -pubout -out pub.pem
echo "ban phat hanh 1.0" > artifact.bin
openssl pkeyutl -sign -inkey priv.pem -rawin -in artifact.bin -out artifact.sig

# verify ban goc -> Verified Successfully
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in artifact.bin -sigfile artifact.sig

# doi 1 byte roi verify lai -> Verification Failure
echo "ban phat hanh 1.1" > artifact.bin
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in artifact.bin -sigfile artifact.sig

Nếu lệnh thứ hai báo thành công còn lệnh thứ ba báo thất bại, bạn vừa tự tay chứng minh: chữ ký biết file đã đổi, kể cả khi bạn chỉ đổi đúng một dòng — và không ai tạo lại được chữ ký đó nếu không có priv.pem trong tay.