sdxc

Type to search, or start from one of these:

[ HTTP & responses ]

@sdxc/digest-fields

RFC 9530 Content-Digest and Repr-Digest, plus the RFC 3230 Digest header

npm add @sdxc/digest-fields
pnpm add @sdxc/digest-fields
yarn add @sdxc/digest-fields
bun add @sdxc/digest-fields

RFC 9530 Content-Digest, Repr-Digest and their Want-* preferences, plus the RFC 3230 Digest header that draft-cavage HTTP signatures cover.

Installation

npm add @sdxc/digest-fields

Every function answers a Result from @sdxc/result, which installs with this package. Digests run on the Web Crypto API.

Usage

import { digest, stringify, verify } from "@sdxc/digest-fields";
import { isFailure, unwrap } from "@sdxc/result";

let body = new TextEncoder().encode(JSON.stringify(activity));
let bytes = unwrap(await digest(body, "sha-256"));

let headers = new Headers({
	"content-digest": unwrap(stringify({ "sha-256": bytes }, "content-digest")), // "sha-256=:…:"
	digest: unwrap(stringify({ "sha-256": bytes }, "digest")), // "SHA-256=…"
});

let checked = await verify(request.headers, await request.bytes(), { field: "content-digest" });
if (isFailure(checked)) return new Response(checked.error.message, { status: 400 });

Reading a field into its value model:

import { parse } from "@sdxc/digest-fields";

parse("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", "content-digest");
// success({ "sha-256": Uint8Array(32) })

parse("sha-512=3, sha-256=10", "want-content-digest");
// success({ "sha-512": 3, "sha-256": 10 })

API

digest(bytes: BinaryLike, algorithm: DigestAlgorithm): Promise<Result<Uint8Array, DigestError>>

Hashes a body with sha-256 or sha-512. Text is read as UTF-8, so pass the exact bytes sent when the body is not UTF-8 text.

let bytes = await digest(body, "sha-256");
// same as
let bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", body));

parse(value: string, field: DigestField): Result<FieldValues[field], DigestError>

Reads a field into digest bytes (content-digest, repr-digest, digest) or preference weights from 0 to 10 (want-content-digest, want-repr-digest), keyed by lowercase algorithm name. The RFC 9530 fields go through the Structured Field grammar of @sdxc/structured-fields. Digest accepts algorithm names in any case, and drops an entry for another algorithm whose value is not base64, such as UNIXsum=30637.

stringify(value: FieldValues[field], field: DigestField): Result<string, DigestError>

Writes a field. Digest uppercases algorithm names (SHA-256=…), which is how every draft-cavage verifier reads them. An empty value answers "", meaning "do not send the field".

verify(headers: Headers, body: BinaryLike, options: { field: DigestValueField }): Promise<Result<void, DigestError>>

Checks a body against content-digest, repr-digest or digest. Every sha-256 and sha-512 entry must match, so a forged entry next to a valid one still fails, and entries for other algorithms are skipped.

DIGEST_ALGORITHMS

["sha-256", "sha-512"], the algorithms digest and verify compute.

DigestError

The failure every function answers with. code says which check failed:

codeWhen
missingverify found no such field
malformedThe field text does not fit its grammar
invalidstringify got a name the field cannot carry or a bad weight
unsupported-algorithmNo sha-256/sha-512 entry to check, or digest got another
mismatchThe body differs from a digest
cryptoThe runtime refused the hash

Types

DigestAlgorithm

"sha-256" | "sha-512".

DigestField

"content-digest" | "repr-digest" | "want-content-digest" | "want-repr-digest" | "digest". DigestValueField is the three that carry digests.

Digests and Preferences

Record<string, Uint8Array> and Record<string, number>, keyed by lowercase algorithm name. FieldValues maps each field to the one it carries.

Pattern: Answering Want-Content-Digest

Send the algorithm the client weighs highest, falling back to sha-256.

import { DIGEST_ALGORITHMS, digest, parse, stringify } from "@sdxc/digest-fields";
import { isSuccess, unwrap } from "@sdxc/result";

function preferred(request: Request) {
	let wanted = parse(request.headers.get("want-content-digest") ?? "", "want-content-digest");
	if (!isSuccess(wanted)) return "sha-256";
	let weights = wanted.data;
	let ranked = DIGEST_ALGORITHMS.filter((name) => (weights[name] ?? 0) > 0);
	ranked.sort((a, b) => (weights[b] ?? 0) - (weights[a] ?? 0));
	return ranked[0] ?? "sha-256";
}

let body = new TextEncoder().encode(JSON.stringify(data));
let algorithm = preferred(request);
let bytes = unwrap(await digest(body, algorithm));

return new Response(body, {
	headers: { "content-digest": unwrap(stringify({ [algorithm]: bytes }, "content-digest")) },
});

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.