digest-fields-parse
Zero-dependency parser and serializer for the HTTP
Content-Digest,Repr-Digest,Want-Content-Digest, andWant-Repr-Digestheader fields defined in RFC 9530 — for Node.js (≥18, ESM + CJS) and Python (≥3.11).
Why
HTTP integrity digests (RFC 9530) let endpoints communicate the integrity of HTTP message content via Content-Digest, Repr-Digest, Want-Content-Digest, and Want-Repr-Digest headers. The headers look like:
Content-Digest: sha-256=:pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:
Want-Content-Digest: sha-256;q=1, sha-512;q=0.5
Most developers today hand-roll this with header.split(','), which silently mishandles multi-algorithm headers, base64 padding inside values, case sensitivity, q-value sorting, and malformed entries. digest-fields-parse handles all four header types correctly in <300 LOC per language, zero runtime dependencies in either package.
Install
Node.js
npm install digest-fields-parse
ESM:
import {
parseContentDigest,
parseReprDigest,
parseWantContentDigest,
parseWantReprDigest,
serializeContentDigest,
serializeReprDigest,
serializeWantContentDigest,
serializeWantReprDigest,
} from 'digest-fields-parse';
CJS:
const {
parseContentDigest,
parseReprDigest,
parseWantContentDigest,
parseWantReprDigest,
serializeContentDigest,
serializeReprDigest,
serializeWantContentDigest,
serializeWantReprDigest,
} = require('digest-fields-parse');
TypeScript definitions are bundled (index.d.ts).
Python
pip install digest-fields-parse
from digest_fields_parse import (
parse_content_digest,
parse_repr_digest,
parse_want_content_digest,
parse_want_repr_digest,
serialize_content_digest,
serialize_repr_digest,
serialize_want_content_digest,
serialize_want_repr_digest,
)
Module ships with py.typed (PEP 561) for full mypy --strict compatibility.
Usage
Parse Content-Digest / Repr-Digest
// Node
parseContentDigest('sha-256=:pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:');
// → Map { 'sha-256' => 'pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4' }
parseContentDigest('sha-256=:abc=, sha-512=:xyz=');
// → Map { 'sha-256' => 'abc', 'sha-512' => 'xyz' }
# Python
parse_content_digest('sha-256=:pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:')
# → OrderedDict([('sha-256', 'pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4')])
parseReprDigest is semantically identical — the field names are separate only because servers distinguish content vs. representation integrity.
Parse Want-Content-Digest / Want-Repr-Digest
parseWantContentDigest('sha-256, sha-512;q=0.5, sha-1;q=1');
// → [
// { algorithm: 'sha-1', q: 1.0, raw: 'sha-1;q=1' },
// { algorithm: 'sha-256', q: 1.0, raw: 'sha-256' },
// { algorithm: 'sha-512', q: 0.5, raw: 'sha-512;q=0.5' },
// ]
// (sorted by descending q; ties keep input order)
parse_want_content_digest('sha-256, sha-512;q=0.5')
# → [
# DigestPreference(algorithm='sha-256', q=1.0, raw='sha-256'),
# DigestPreference(algorithm='sha-512', q=0.5, raw='sha-512;q=0.5'),
# ]
Serialise
serializeContentDigest(new Map([['sha-256', 'pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4']]));
// → 'sha-256=:pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4:'
serializeWantContentDigest([
{ algorithm: 'sha-256', q: 1.0 },
{ algorithm: 'sha-512', q: 0.5 },
]);
// → 'sha-256;q=1, sha-512;q=0.5'
Round-trip
const m = parseContentDigest(wire);
const sameWire = serializeContentDigest(m);
parseContentDigest(sameWire); // === m
Semantics
| Header | Wire form | Returned type (Node) | Returned type (Python) |
|---|---|---|---|
Content-Digest / Repr-Digest |
alg=:base64:,( alg=:base64:)* |
Map<string, string> |
OrderedDict[str, str] |
Want-Content-Digest / Want-Repr-Digest |
alg[;q=N.NNN][, …] |
Array<{algorithm, q, raw}> |
list[DigestPreference] |
- Base64 padding. Trailing
=padding inside the value is stripped (per RFC 9530 §2.2). Internal=characters (which are part of the base64 alphabet) are preserved. - Algorithm case. Algorithm names are case-sensitive.
SHA-256andsha-256are distinct keys. - Multi-algorithm. A single header may contain multiple
alg=:base64:pairs separated by commas. The parser uses insertion-order; the last duplicate wins (matches the spec example). - Empty header.
Content-Digest:(empty value) returns an empty Map / OrderedDict. - Malformed entries. Garbage that lacks the
=:delimiters is silently dropped, not raised — matches the typical HTTP-header parser philosophy (lenient on input, strict on output). - Want q sorting. Preferences are sorted by descending
q. Ties keep input order (stable sort). Defaultqis1.0when;q=is omitted. - Want q format. Strict
[0, 1]decimal matching^(0(\.\d{1,3})?|1(\.0{1,3})?)$. Malformed q-values fall back to1.0(no silentparseFloatconsumption). - Want serializer canonical form.
serializeWant*always emits explicit;q=N(matches spec AC-16 wire form) so parse ∘ serialize is a stable round-trip.
Error handling
| Call | Behaviour |
|---|---|
parse*(null) |
throws TypeError |
parse*(undefined) |
throws TypeError |
parse*(<non-string>) |
throws TypeError |
parse*(<empty string>) |
returns empty Map / [] |
parse*(<malformed entry>) |
silently skips bad entries, returns partial result |
serialize*(null) |
throws TypeError |
serialize*({}) |
returns empty string (valid) |
Tests
Both languages ship a deep-coverage test suite that exercises all 33 spec acceptance criteria plus regression cases for every parser/serializer edge case.
155 tests (73 Node.js + 82 Python)
|| Suite | Command | Test count |
|| --- | --- | --- |
|| Node | npm test | 73 tests |
|| Python | pytest | 82 tests |
Type-checking is part of the test contract:
# Node — bundled types
npx --yes tsc --noEmit index.d.ts
# Python — strict static typing
mypy --strict digest_fields_parse.py
Non-goals
Explicitly out of scope:
- No crypto. This library parses and serialises the wire format only. It does NOT compute or verify digest hashes. Pair it with
crypto.createHash('sha256')in Node orhashlib.sha256()in Python if you need to compute digests. - No RFC 3230. The older
Digest/Want-Digestheaders (RFC 3230) are obsolete and use a different syntax. This package implements only the four RFC 9530 header types. - No RFC 9421. HTTP Message Signatures are a separate standard with a different scope.
- No
Content-MD5. The MD5-based header (RFC 1864) is deprecated and not part of RFC 9530. - No streaming. Buffers must be in memory; no incremental parsing API.
Known limitations / Cross-runtime parity
These are documented divergences and accepted trade-offs for v0.1.0. None of them affect correctly-encoded ASCII inputs (which is everything RFC 9530 §5 examples and §2.2 normative ABNF produce). They are confined to pathological inputs that RFC 9530 does not constrain.
L-1 — Lenient on control characters inside opaque-tag / digest-value / algorithm-name
RFC 9530 §2.2 specifies opaque-tag = ALPHA *( ALPHA / DIGIT / "-" ) and algorithm-name = token (RFC 9110), both of which exclude CTL bytes (\x00–\x1F, \x7F). v0.1.0's parser is lenient and preserves CTL bytes verbatim on round-trip, so an attacker who controls header input could pass a CRLF-containing value that, if a downstream consumer concatenates library output into an HTTP header context, could enable header injection.
Mitigation in your code: before serializing library output into a header, validate that the algorithm-name, opaque-tag, and digest-value do not contain CR (\r), LF (\n), or NUL (\x00). The library deliberately does NOT enforce this because it is not itself a header-writing sink — it is the consumer's responsibility to validate before serialization.
L-2 — Cross-runtime: U+FEFF (\uFEFF, ZERO WIDTH NO-BREAK SPACE) handling differs
V8's String.prototype.trim() strips U+FEFF; CPython's str.strip() does not. This causes up to 138 documented parity divergences on fuzz inputs that mix U+FEFF with ASCII whitespace at token boundaries (opaque-tag positions, algorithm-name positions, and around ;q= values).
Impact: zero on RFC 9530 §5 documented inputs. Documented in VULN_AUDIT.md (cycle_39/05 addendum) as findings F-04 and F-05 (both Low). The library is lenient on both runtimes; if you need strict parity, normalize U+FEFF in your input before parsing:
// Node
parseContentDigest(input.replace(/\uFEFF/g, ''));
# Python
parse_content_digest(input.replace('\uFEFF', ''))
L-3 — RFC 9530 §2.2 conformance is "lenient-on-input, strict-on-output" only for ASCII
The parser silently skips malformed entries (entries lacking the =: delimiters, or with invalid q-values). This matches typical HTTP-header-parser philosophy and is intentional, but it means the parser does NOT raise on RFC 9530 §2.2 violations for CTL bytes (see L-1). If you need strict ABNF validation, run the output of parse* through your own validator before serializing.
Competitive landscape
| Package | Why it doesn't fit |
|---|---|
@misskey-dev/node-http-message-signatures |
Scoped package; non-zero deps (WebCrypto); focus on signing (RFC 9421), not digest-field parsing |
@shujaapay/http-message-signatures |
Scoped; focus on RFC 9421 + GNAP signing; no dedicated Content-Digest parsing API |
http-digest (npm) |
Pre-RFC-9530; implements the obsolete RFC 3230 Digest header |
Hand-rolled header.split(',') |
Silently fails on multi-algorithm, base64 padding, q-values, mixed-case |
| (No PyPI equivalent) | No prior Python package exists for RFC 9530 digest fields |
License
MIT. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file digest_fields_parse-0.1.1.tar.gz.
File metadata
- Download URL: digest_fields_parse-0.1.1.tar.gz
- Upload date:
- Size: 21.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f22e698b51dc01e683baaca5cf9832456fe5124089c6a391efe94f8765eba3ca
|
|
| MD5 |
10bd54a89fdc61be4d9e0ffad28c2a24
|
|
| BLAKE2b-256 |
6b61a2816141abbfad262f09eb4e1975e6e88b38dc2fbe49fc052f0b7282c7af
|
File details
Details for the file digest_fields_parse-0.1.1-py3-none-any.whl.
File metadata
- Download URL: digest_fields_parse-0.1.1-py3-none-any.whl
- Upload date:
- Size: 9.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33ee6e77435a2deb4b637a94ebdbcaef81b59c1348a5cbbb453303e49ee6ab53
|
|
| MD5 |
c420e766646923b4982c38871c755025
|
|
| BLAKE2b-256 |
4dd244e69635e4d0ed36170ec8a1f3c78101f66586750f98628655085917bbc8
|