Skip to content

nginx module for RFC 9530 Digest Fields — adds Content-Digest and Repr-Digest headers using sidecar checksum files

License

Notifications You must be signed in to change notification settings

ptudor/ngx_http_digest_fields_module

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ngx_http_digest_fields_module

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.

Live Example

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.

See It Work

Server side: publish a file with its checksum

# 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.txt

Add 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.

Client side: verify the download

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.

Compressed files: both headers at once

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 original
location /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 of archive.tar.gz (the bytes on the wire)
  • Repr-Digest = hash of archive.tar (the uncompressed representation)

Features

  • RFC 9530 compliant Content-Digest header 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

Build

Requires an nginx source tree. The module supports both dynamic and static compilation.

Dynamic module (recommended)

cd /path/to/nginx-source
./configure --add-dynamic-module=/path/to/ngx_http_digest_fields_module
make modules

Install the resulting .so:

cp objs/ngx_http_digest_fields_module.so /usr/local/libexec/nginx/

Static module

cd /path/to/nginx-source
./configure --add-module=/path/to/ngx_http_digest_fields_module
make
make install

Configuration

Load 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;
        }
    }
}

Directives

digest_fields

Syntax: digest_fields on|off; Default: off Context: http, server, location

Enables or disables the module.

digest_fields_repr

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.

digest_fields_algorithm

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.

digest_fields_match

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)$";

digest_fields_directory

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;

digest_fields_compression

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;

Sidecar Files

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.

Generating Sidecar Files

# 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.sha256

On FreeBSD, use shasum -a 256 or sha256 instead of sha256sum. On macOS, use shasum -a 256. All output formats are supported.

Repr-Digest sidecars

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

Example Configurations

Basic

location /downloads/ {
    digest_fields on;
}

Mirror server with Repr-Digest

location /mirror/ {
    digest_fields on;
    digest_fields_repr on;
    gzip off;
}

SHA-512 preferred, SHA-256 fallback

location /secure/ {
    digest_fields on;
    digest_fields_algorithm sha-512;
    digest_fields_algorithm sha-256;
}

Filtered by file type

location /releases/ {
    digest_fields on;
    digest_fields_repr on;
    digest_fields_match "\.(tar\.gz|tar\.xz|zip)$";
}

Sidecar subdirectory

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

Want-Content-Digest / Want-Repr-Digest

Clients can request specific algorithms per RFC 9530:

curl -H "Want-Content-Digest: sha-512=1, sha-256=0.5" http://localhost/files/data.json

The 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.

Important Notes

gzip incompatibility

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;

Static files only

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).

Companion Apache Module

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

References

License

Apache License 2.0. See LICENSE.

About

nginx module for RFC 9530 Digest Fields — adds Content-Digest and Repr-Digest headers using sidecar checksum files

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages