Skip to main content

digest-fields-parse

Zero-dependency parser and serializer for the HTTP Content-Digest, Repr-Digest, Want-Content-Digest, and Want-Repr-Digest header fields defined in RFC 9530 — for Node.js (≥18, ESM + CJS) and Python (≥3.11).

npm pypi license tests


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-256 and sha-256 are 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). Default q is 1.0 when ;q= is omitted.
  • Want q format. Strict [0, 1] decimal matching ^(0(\.\d{1,3})?|1(\.0{1,3})?)$. Malformed q-values fall back to 1.0 (no silent parseFloat consumption).
  • 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 or hashlib.sha256() in Python if you need to compute digests.
  • No RFC 3230. The older Digest / Want-Digest headers (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

digest_fields_parse-0.1.0.tar.gz (21.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

digest_fields_parse-0.1.0-py3-none-any.whl (9.2 kB view details)

Uploaded Python 3

File details

Details for the file digest_fields_parse-0.1.0.tar.gz.

File metadata

  • Download URL: digest_fields_parse-0.1.0.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

Hashes for digest_fields_parse-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b2e07667fd593101d6721029fe276a1def25e876e646b73c4beab08fe87311d5
MD5 73799a8a89f9cb36acc3f1a9f3d7ad15
BLAKE2b-256 54b64168a19caaf3a8bf1e0746d2b22633462d8ad8f1ad68b794d13376c7c8c3

See more details on using hashes here.

File details

Details for the file digest_fields_parse-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for digest_fields_parse-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 70208fbdcf36fc4971ec45ace15594a03411475fe3b9a5ea0abf4bf98ea25add
MD5 ddb02b75f7d05e0b1327c0b62a418658
BLAKE2b-256 78137d83857797114ac97f267ee8c7fb03f6ae94a17c5143df8eca714ac7461e

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page