An nginx module implementing RFC 9530 (Digest Fields). Adds Content-Digest and Repr-Digest response headers for static files using pre-computed sidecar checksum files.
$ curl -si http://localhost/mirror/myapp-2.1.tar.gz
HTTP/1.1 200 OK
Content-Digest: sha-256=:VnYLtiDf1sVMSehoMeoYSw0u0cHNKh7A+4XSmaGSpEc=:
Content-Length: 10485760
Content-Type: application/gzip
The client has a cryptographic hash of the file before it even finishes downloading.
This is a header filter module -- it adds headers non-destructively to responses already being served by nginx's static file handler.
https://www.any53.com/ uses the companion mod_digest_fields Apache module in production. The nginx module produces identical headers from the same sidecar files.
# Create a file to serve
echo "Hello, RFC 9530!" > /var/www/files/greeting.txt
# Create the sidecar checksum (one command)
sha256sum /var/www/files/greeting.txt > /var/www/files/greeting.txt.sha256
# That's it. The sidecar file looks like this:
cat /var/www/files/greeting.txt.sha256
# 1bfa3474a00891becf8e2b8b3bba5da1e3ed25da51e6a09988e72cce74c1d758 /var/www/files/greeting.txtAdd the module to your nginx config:
load_module modules/ngx_http_digest_fields_module.so;
http {
server {
location /files/ {
digest_fields on;
gzip off;
}
}
}Reload nginx, then request the file:
$ curl -si http://localhost/files/greeting.txt
HTTP/1.1 200 OK
Content-Digest: sha-256=:G/o0dKAIkb7PjiuLO7pdoePtJdpR5qCZiOcsznTB11g=:
Content-Length: 18
Content-Type: text/plain
Hello, RFC 9530!
The Content-Digest header is the SHA-256 hash of the file, base64-encoded per RFC 8941 Structured Fields.
The companion Apache module includes a verify.py script that downloads a file and checks it against the Content-Digest header in one step. It works identically with the nginx module:
$ python3 verify.py http://localhost/files/greeting.txt
Fetching http://localhost/files/greeting.txt ...
Content-Digest: sha-256=:G/o0dKAIkb7PjiuLO7pdoePtJdpR5qCZiOcsznTB11g=:
Algorithm : sha-256
Expected : 1bfa3474a00891becf8e2b8b3bba5da1e3ed25da51e6a09988e72cce74c1d758
Got : 1bfa3474a00891becf8e2b8b3bba5da1e3ed25da51e6a09988e72cce74c1d758
MATCH -- file integrity verified.
No dependencies beyond Python 3.
For mirror servers hosting pre-compressed archives, the module can emit both headers -- one for the compressed file on disk, and one for the uncompressed original:
# On the server
sha256sum archive.tar.gz > archive.tar.gz.sha256 # hash of compressed file
sha256sum archive.tar > archive.tar.sha256 # hash of originallocation /mirror/ {
digest_fields on;
digest_fields_repr on;
gzip off;
}$ curl -si http://localhost/mirror/archive.tar.gz
HTTP/1.1 200 OK
Content-Digest: sha-256=:kF3mVaSgX1OwBxHj6qMKQRjOCPRtkHmP7mNJKAergNQ=:
Repr-Digest: sha-256=:G/o0dKAIkb7PjiuLO7pdoePtJdpR5qCZiOcsznTB11g=:
Content-Digest= hash ofarchive.tar.gz(the bytes on the wire)Repr-Digest= hash ofarchive.tar(the uncompressed representation)
- RFC 9530 compliant
Content-Digestheader with RFC 8941 Structured Field byte sequence format - Repr-Digest header for uncompressed content representation (optional)
- Want-Content-Digest / Want-Repr-Digest client preference headers with weighted algorithm selection
- SHA-256 and SHA-512 (no deprecated algorithms)
- Flexible sidecar parsing: GNU coreutils, BSD, and plain hex formats
- Sidecar subdirectory support (
digest_fields_directory) - Filename regex filtering (
digest_fields_match) - Configurable compression extensions for Repr-Digest stripping
Requires an nginx source tree. The module supports both dynamic and static compilation.
cd /path/to/nginx-source
./configure --add-dynamic-module=/path/to/ngx_http_digest_fields_module
make modulesInstall the resulting .so:
cp objs/ngx_http_digest_fields_module.so /usr/local/libexec/nginx/cd /path/to/nginx-source
./configure --add-module=/path/to/ngx_http_digest_fields_module
make
make installLoad the dynamic module, then enable it in any http, server, or location block:
load_module modules/ngx_http_digest_fields_module.so;
http {
server {
location /mirror/ {
digest_fields on;
gzip off;
}
}
}Syntax: digest_fields on|off;
Default: off
Context: http, server, location
Enables or disables the module.
Syntax: digest_fields_repr on|off;
Default: off
Context: http, server, location
Enables the Repr-Digest header for uncompressed content. When a compressed file is served (e.g., file.tar.gz), the module strips the compression extension and looks for the base file's sidecar (file.tar.sha256) to produce the Repr-Digest header.
Syntax: digest_fields_algorithm sha-256|sha-512;
Default: sha-256
Context: http, server, location
Adds an algorithm to the preference list. May be specified multiple times; the module tries each in order and uses the first sidecar found. Duplicate entries are ignored with a warning.
Syntax: digest_fields_match <regex>;
Default: none (all files matched)
Context: http, server, location
Only add digest headers for files whose basename matches the given PCRE regex. The match is case-sensitive. Requires nginx built with PCRE support.
digest_fields_match "\.(tar\.gz|tar\.xz|zip|iso)$";Syntax: digest_fields_directory <name>;
Default: none (sidecars alongside files)
Context: http, server, location
Look for sidecar files in a subdirectory instead of alongside the served file. The name must not contain path separators or be . or ...
# /var/www/mirror/file.tar.gz -> /var/www/mirror/.checksum/file.tar.gz.sha256
digest_fields_directory .checksum;Syntax: digest_fields_compression <ext> [ext ...];
Default: gz bz2 xz zst lz4 lzma lzfse br
Context: http, server, location
Override the list of compression extensions used for Repr-Digest stripping. Specify bare names without dots. Matching is case-insensitive.
digest_fields_compression gz bz2 zst;Each file's checksum lives in a sidecar with the same name plus .sha256 or .sha512:
file.tar.gz -> file.tar.gz.sha256
file.tar.gz -> file.tar.gz.sha512
The module parses common checksum file formats:
# Plain hex
5676bbb620dfd6c54c49e86831ea2577aa8d9cbc7e6ad5ea1f6848e9bc4f69fa
# GNU coreutils (sha256sum)
5676bbb620dfd6c54c49e86831ea2577aa8d9cbc7e6ad5ea1f6848e9bc4f69fa file.tar.gz
# BSD (shasum -a 256, openssl dgst)
SHA256 (file.tar.gz) = 5676bbb620dfd6c54c49e86831ea2577aa8d9cbc7e6ad5ea1f6848e9bc4f69fa
Strict validation: sidecar files must be a single line under 1024 bytes containing exactly one hex string of the expected length. Ambiguous or malformed files are silently skipped.
# Single file
sha256sum myapp-2.1.tar.gz > myapp-2.1.tar.gz.sha256
# All files in a directory
for f in /var/www/mirror/*.tar.gz; do
sha256sum "$f" > "$f.sha256"
done
# Into a subdirectory
mkdir -p /var/www/mirror/.checksum
for f in /var/www/mirror/*.tar.gz; do
sha256sum "$f" > "/var/www/mirror/.checksum/$(basename "$f").sha256"
done
# Both compressed and uncompressed (for Repr-Digest)
sha256sum archive.tar > archive.tar.sha256
sha256sum archive.tar.gz > archive.tar.gz.sha256On FreeBSD, use shasum -a 256 or sha256 instead of sha256sum. On macOS, use shasum -a 256. All output formats are supported.
For Repr-Digest, the module strips the compression extension to find the base file's sidecar:
| Requested File | Content-Digest from | Repr-Digest from |
|---|---|---|
file.tar.gz |
file.tar.gz.sha256 |
file.tar.sha256 |
data.json.zst |
data.json.zst.sha256 |
data.json.sha256 |
location /downloads/ {
digest_fields on;
}location /mirror/ {
digest_fields on;
digest_fields_repr on;
gzip off;
}location /secure/ {
digest_fields on;
digest_fields_algorithm sha-512;
digest_fields_algorithm sha-256;
}location /releases/ {
digest_fields on;
digest_fields_repr on;
digest_fields_match "\.(tar\.gz|tar\.xz|zip)$";
}Keeps directory listings clean by hiding sidecar files:
location /mirror/ {
digest_fields on;
digest_fields_directory .checksum;
gzip off;
}Sidecar layout:
mirror/
myapp-2.1.tar.gz
myapp-2.0.tar.gz
.checksum/
myapp-2.1.tar.gz.sha256
myapp-2.0.tar.gz.sha256
Clients can request specific algorithms per RFC 9530:
curl -H "Want-Content-Digest: sha-512=1, sha-256=0.5" http://localhost/files/data.jsonThe module parses algorithm preferences with weights (0.0-1.0), filters to server-configured algorithms, and uses the client's preferred order. Weight 0 is an explicit opt-out.
If nginx's gzip module compresses the response, the bytes sent to the client will not match the Content-Digest (which reflects the original file on disk). Disable gzip in locations where digest_fields is enabled:
gzip off;This module is designed for static file serving. It constructs sidecar paths from the filesystem path resolved by nginx. It will not produce useful results for proxied or dynamically generated responses (it silently skips them when no sidecar is found).
This module is designed to be interchangeable with mod_digest_fields for Apache. Both use the same sidecar file format and produce identical headers. Sidecar files created for one server work with the other.
| Feature | Apache (mod_digest_fields) | nginx (this module) |
|---|---|---|
| Sidecar format | .sha256 / .sha512 |
.sha256 / .sha512 |
| Sidecar parsing | GNU, BSD, plain hex | GNU, BSD, plain hex |
| Header output | sha-256=:base64: |
sha-256=:base64: |
| Repr-Digest | Yes | Yes |
| Want-Content-Digest | Yes | Yes |
| Sidecar subdirectory | Yes | Yes |
| Filename match regex | Yes | Yes |
| Custom compression list | Yes | Yes |
| Client verification | verify.py |
verify.py |
Apache License 2.0. See LICENSE.