Lập trình · 05/10/2026

HTTP Content-Digest thực chiến: Kiểm tra toàn vẹn body theo RFC 9530

TLS bảo vệ từng kết nối, nhưng một HTTP message có thể đi qua nhiều proxy, gateway và bước biến đổi trước khi tới ứng dụng. RFC 9530 định nghĩa Content-Digest và Repr-Digest để endpoint kiểm tra byte nhận được có còn khớp với digest do bên gửi công bố hay không.

HTTP Content-Digest thực chiến: Kiểm tra toàn vẹn body theo RFC 9530

TLS bảo vệ từng kết nối, nhưng một HTTP message có thể đi qua nhiều proxy, gateway và bước biến đổi trước khi tới ứng dụng. RFC 9530 định nghĩa Content-Digest và Repr-Digest để endpoint kiểm tra byte nhận được có còn khớp với digest do bên gửi công bố hay không.

Bài viết phân biệt content với selected representation, tạo và xác minh SHA-256, xử lý nén, Range, HEAD, trailer, proxy và security policy. Nội dung dựa trên RFC 9530: Digest Fields và định dạng Byte Sequence của RFC 8941: Structured Field Values for HTTP. RFC 9530 thay thế field Digest/Want-Digest cũ của RFC 3230.

1. Digest field giải quyết bài toán nào?

Sender băm dữ liệu bằng thuật toán đã đăng ký, đặt tên thuật toán và kết quả vào HTTP field. Receiver băm lại đúng chuỗi byte tương ứng rồi so sánh. Nếu khác nhau, nội dung đã bị hỏng, bị biến đổi ngoài dự kiến hoặc hai phía đang hiểu sai điểm đặt digest.

Cơ chế hữu ích cho upload API, artifact, webhook body, file lớn, dữ liệu đi qua nhiều hop và chẩn đoán proxy transformation. Nó không thay thế validation theo schema: JSON có digest đúng vẫn có thể chứa dữ liệu nghiệp vụ sai.

2. Cú pháp không phải chuỗi checksum tùy ý

Content-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:

Content-Digest: sha-256=:...:, sha-512=:...:

Field là một Structured Fields Dictionary. Key là tên thuật toán; value là Byte Sequence: byte digest được Base64 và bao bởi dấu hai chấm. Hai dấu hai chấm là một phần cú pháp, không phải một phần của Base64.

Không nên tự split(',') hoặc viết regex lỏng cho production. Dùng parser RFC 8941, giới hạn số member/kích thước field và từ chối duplicate hay syntax lỗi theo policy ứng dụng.

3. Content-Digest và Repr-Digest khác nhau

FieldDữ liệu được bămTình huống điển hình
Content-DigestContent thực tế của messageKiểm tra body upload/response vừa truyền
Repr-DigestToàn bộ selected representationHEAD, Range, ghép nhiều phần hoặc xác nhận representation của resource

Với response 200 chứa toàn bộ representation và không có biến đổi đặc biệt, hai giá trị có thể giống nhau. Với response 206, Content-Digest chỉ băm phần byte gửi trong message, còn Repr-Digest có thể mô tả toàn bộ selected representation nên khác nhau.

Với HEAD, content rỗng nên Content-Digest — nếu gửi — là digest của chuỗi rỗng; Repr-Digest vẫn có thể mô tả representation tương ứng với GET.

4. Sinh Content-Digest trong PHP

<?php
$body = json_encode(
    ['id' => 123, 'status' => 'ready'],
    JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
);

$rawDigest = hash('sha256', $body, true);
$encoded = base64_encode($rawDigest);

header('Content-Type: application/json');
header('Content-Digest: sha-256=:' . $encoded . ':');
header('Content-Length: ' . strlen($body));
echo $body;

Phải băm đúng byte được gửi. Đừng băm object trước rồi serializer thêm khoảng trắng, đổi escape, newline hoặc thứ tự key. Hãy serialize một lần, băm buffer cuối và gửi chính buffer đó.

Với file lớn, dùng streaming hash context thay vì nạp toàn bộ file vào RAM:

$ctx = hash_init('sha256');
$stream = fopen($path, 'rb');

while (!feof($stream)) {
    $chunk = fread($stream, 1024 * 1024);
    hash_update($ctx, $chunk);
    // Ghi chunk vào response nếu kiến trúc hỗ trợ trailer.
}

$digest = base64_encode(hash_final($ctx, true));

5. Xác minh request trước khi parse

  1. Giới hạn kích thước body và header trước khi băm.
  2. Thu đúng raw content bytes tại tầng còn nhìn thấy chúng.
  3. Parse field bằng Structured Fields parser.
  4. Chọn thuật toán mạnh được policy cho phép, ví dụ SHA-256 hoặc SHA-512.
  5. Tính digest streaming và Base64-decode giá trị nhận được.
  6. So sánh byte bằng hàm constant-time.
  7. Chỉ sau khi khớp mới giải nén, parse JSON và chạy side effect.
// Pseudocode Node.js cho một giá trị sha-256 đã parse chuẩn
const actual = createHash('sha256').update(rawContentBytes).digest();
const expected = parsedDictionary.get('sha-256'); // Uint8Array

if (actual.length !== expected.length ||
    !timingSafeEqual(actual, expected)) {
  throw new BadRequest('content digest mismatch');
}

Không parse JSON rồi stringify lại để kiểm tra: hai JSON có cùng dữ liệu logic nhưng byte khác nhau cho digest khác nhau. Integrity ở đây gắn với byte và representation metadata, không phải canonical JSON.

6. Nén là nơi triển khai thường sai

Content-Encoding: gzip hoặc br là một phần semantics của representation. Digest phải khớp dữ liệu tại điểm RFC yêu cầu; nếu reverse proxy nén response sau khi application tính digest trên JSON chưa nén, field và body sẽ không còn đồng bộ.

Chọn một owner cuối cùng cho transformation:

  • Nếu application tạo digest, giữ nguyên bytes sau đó và tắt nén/biến đổi downstream cho route đó.
  • Nếu gateway nén, để gateway tính lại digest trên output cuối.
  • Nếu cần Repr-Digest, metadata như Content-Encoding và Content-Type phải nhất quán với representation được băm.

Cùng input nhưng gzip level, timestamp hoặc nonce khác có thể tạo output byte khác. Digest của transfer không mặc nhiên là bằng chứng integrity của file “at rest” nếu không lưu chính byte đã băm.

7. Range, HEAD và response không content

HTTP/1.1 206 Partial Content
Content-Range: bytes 10-18/19
Content-Digest: sha-256=:digest-of-these-9-bytes:
Repr-Digest: sha-256=:digest-of-full-selected-representation:

Đây là lý do RFC cung cấp hai field. Client tải file bằng nhiều range có thể kiểm tra từng message bằng Content-Digest và kiểm tra representation hoàn chỉnh bằng Repr-Digest nếu server cung cấp đúng contract.

Response 204 hoặc HEAD không có content vẫn có thể có digest của empty content. Đừng lấy Content-Length mô tả GET làm input băm cho HEAD; input Content-Digest của message HEAD vẫn rỗng.

8. Header hay trailer?

Header yêu cầu biết digest trước khi gửi header, phù hợp khi body đã nằm trong buffer/file hoặc digest đã được tính trước. Trailer cho phép băm khi stream:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Trailer: Content-Digest

...chunks...
0
Content-Digest: sha-256=:...:

Trade-off quan trọng: intermediary được phép bỏ trailer, và receiver chỉ nhận digest sau body. Nếu consumer đã parse, lưu hoặc kích hoạt side effect trước khi trailer đến, validation thất bại là quá muộn. Với dữ liệu nhạy cảm, buffer/quarantine tới khi xác minh xong hoặc dùng protocol xử lý streaming có rollback rõ ràng.

9. Đàm phán bằng Want-Content-Digest

Want-Content-Digest: sha-512=3, sha-256=10
Want-Repr-Digest: sha-256=10

Trọng số là integer 0–10: 10 ưu tiên cao nhất, 1 thấp nhất, 0 là không chấp nhận. Đây chỉ là hint; phía nhận có thể bỏ qua, chọn thuật toán khác hoặc không gửi digest. API yêu cầu bắt buộc phải định nghĩa contract riêng và trả lỗi rõ khi field/algorithm thiếu.

SHA-256 và SHA-512 đang ở trạng thái active trong registry của RFC 9530. MD5, SHA-1 và các checksum Unix là deprecated; không chấp nhận thuật toán yếu chỉ vì client gửi nó đầu tiên.

10. Digest không phải chữ ký, HMAC hay TLS

  • Digest phát hiện mismatch, nhưng bất kỳ ai sửa body cũng có thể tính digest mới nếu field không được bảo vệ.
  • Nó không xác thực sender, không cấp quyền và không giữ bí mật nội dung.
  • TLS bảo vệ từng connection và chống attacker trên đường truyền trong phạm vi trust model của TLS.
  • HTTP Message Signatures có thể ký digest field cùng metadata quan trọng để chống thay thế.
  • HMAC ở application layer có thể cung cấp xác thực shared-secret cho một contract cụ thể.

Nếu threat model có attacker chủ động, “SHA-256 đúng” chưa đủ. Policy phải bảo vệ cả digest field, algorithm choice, content type/encoding và metadata được dùng để diễn giải content.

11. Khác ETag và Subresource Integrity

ETag là validator cho cache/conditional request; nó có thể opaque hoặc weak và không có nghĩa mặc định là checksum mà client phải xác minh. Subresource Integrity đặt expected hash trong HTML để browser kiểm tra script/stylesheet tải từ URL. Digest Fields đi cùng HTTP message và áp dụng rộng cho request/response content hoặc representation.

Ba cơ chế có thể cùng tồn tại nhưng không hoán đổi contract. Đừng lấy một ETag dạng hash rồi giả định nó tuân theo cú pháp hoặc security semantics của RFC 9530.

12. Proxy, cache và quan sát production

  • Inventory mọi tầng có thể nén, giải nén, minify, transcode hoặc đổi line ending.
  • Test cả identity, gzip, Brotli, HEAD, 206, 204, error response và cache hit/miss.
  • Không log raw body hoặc full digest nếu dữ liệu nhạy cảm; log algorithm, result, route, hop và request ID.
  • Metric: missing digest, unsupported algorithm, parse failure, mismatch, verification bytes/time và trailer missing.
  • Giới hạn số algorithm để tránh attacker ép server băm body lớn nhiều lần.
  • Cache variant phải khớp Vary, content coding và digest tương ứng.

Khi mismatch, giữ bằng chứng tối thiểu an toàn: request ID, độ dài, algorithm, vị trí hệ thống và hash cục bộ; không tự động retry side effect không idempotent.

Checklist production

  • Dùng field RFC 9530 mới, không phát sinh mới Digest/Want-Digest cũ.
  • Serialize một lần, băm và gửi đúng cùng chuỗi byte.
  • Dùng Structured Fields parser và constant-time comparison.
  • Chỉ chấp nhận algorithm mạnh theo allowlist.
  • Xác minh trước parse hoặc side effect; body lớn được băm streaming có giới hạn.
  • Compression/transformation có một owner chịu trách nhiệm cập nhật digest.
  • Range, HEAD, empty content và trailer có test riêng.
  • Digest bắt buộc được ghi thành API contract, không dựa vào Want-* hint.
  • TLS/chữ ký/HMAC bảo vệ threat model xác thực; digest không bị dùng sai vai trò.
  • Mismatch, missing field và unsupported algorithm có metric/cảnh báo.

Kết luận

RFC 9530 cung cấp một contract chuẩn để kiểm tra toàn vẹn byte ở tầng HTTP. Content-Digest phù hợp cho body của message đang truyền; Repr-Digest giải quyết representation hoàn chỉnh, đặc biệt với HEAD và Range. Triển khai đúng không dừng ở gọi SHA-256: phải chọn đúng byte trước/sau content coding, parse Structured Fields, kiểm tra trước side effect, kiểm soát trailer/proxy và kết hợp TLS hoặc chữ ký khi cần chống attacker chủ động.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.