DOM canonicalization spec
This spec defines how Fidemark turns a DOM subtree (a “region”) into a deterministic UTF-8 string for hashing. Every SDK and the browser extension implement the same rules; otherwise publisher hashes and verifier hashes drift and trust collapses.
The spec is frozen at version 1. Any change requires bumping FIDEMARK_DOM_VERSION and a parallel migration so old attestations can still be reproduced. Silent rule changes are a protocol break.
Why a custom spec
Section titled “Why a custom spec”Off-the-shelf canonicalizers (XML c14n, JSON canonicalization) are wrong tools here:
- XML c14n preserves attributes and tags, which we want to ignore (class renames, style refactors, ad slots).
- JSON canonicalization assumes structured data, not human-readable content.
innerTextandtextContentdiffer between browsers and break on whitespace runs.
The spec below is intentionally narrow: it captures what a reader sees, not what the markup says.
Rules (v1)
Section titled “Rules (v1)”- Walk the subtree depth-first.
- Skip entirely:
<script>,<style>,<noscript>,<template>, HTML comments, and any element bearing[data-fidemark-ignore](along with all its descendants). - Each surviving text node contributes its raw
nodeValueas a “text” segment. - After exiting any block-level element (see table below), emit one “break” segment.
- Build the canonical string:
- For each text segment, replace runs of ASCII whitespace (space, tab, CR, LF, FF) with a single space.
- For each break segment, emit a literal
\n. - Concatenate the segments.
- Collapse runs of
\nto a single\n. - Trim leading and trailing spaces from each line.
- Drop leading and trailing blank lines.
- NFC-normalize the result.
The hash is SHA-256 of the resulting UTF-8 bytes, encoded as a 0x-prefixed lowercase 32-byte hex digest.
Block-level elements
Section titled “Block-level elements”Each emits a single break after walking its children:
BR, P, DIV, LI, H1, H2, H3, H4, H5, H6, ARTICLE, SECTION, HEADER, FOOTER, BLOCKQUOTE, PRE, TABLE, TR.
Tag matching is case-insensitive. Element names not in this list contribute nothing of their own; their text descendants pass through unchanged.
Skipped elements
Section titled “Skipped elements”Subtrees of these are dropped entirely:
SCRIPT, STYLE, NOSCRIPT, TEMPLATE. Plus any element matching [data-fidemark-ignore] (the attribute applies to the element AND its descendants).
Test vectors
Section titled “Test vectors”Every SDK ships the same fixture file (sources/sdk/_fixtures/dom-canonicalization.json). A few rows:
Input HTML (with selector #root) | Canonical text | SHA-256 |
|---|---|---|
<article id="root"><p>Hello world.</p></article> | Hello world. | 0xaa3ec16e...d115d11 |
<article id="root"><p>First.</p><p>Second.</p></article> | First.\nSecond. | 0x31cf545a...d9c30 |
<p id="root"> a \t b \n c </p> | a b c | 0xb771ebfb...2466ed |
<article id="root"><script>alert('x')</script><p>visible</p></article> | visible (script dropped) | 0x06c5928b...c043b |
<article id="root"><p>keep</p><div data-fidemark-ignore><p>drop</p></div><p>keep too</p></article> | keep\nkeep too | 0xaa8248f7...de863 |
When you implement a new SDK in another language, the fixture file is your acceptance test: every case must produce the listed canonical text and hash.
Computing the hash
Section titled “Computing the hash”import { canonicalizeDom, hashRegion } from "@fidemark/sdk/dom";
const region = document.querySelector("article#post-42")!;console.log(canonicalizeDom(region));console.log(await hashRegion(region));from fidemark import canonicalize_dom, hash_region
with open("post.html", "r", encoding="utf-8") as f: html = f.read()
print(canonicalize_dom(html, "#post-42"))print(hash_region(html, "#post-42"))import "github.com/fidemark/sdk-go/fidemark"
text, _ := fidemark.CanonicalizeDOM(htmlString, "#post-42")hash, _ := fidemark.HashRegion(htmlString, "#post-42")Stability guarantees
Section titled “Stability guarantees”- The rules above are version 1. They will not change without a
FIDEMARK_DOM_VERSIONbump. - The shared fixture file is the source of truth for cross-language parity; CI for each SDK runs against it.
- Hashes computed today will continue to match new SDK builds as long as
FIDEMARK_DOM_VERSIONis 1, regardless of patch-level updates to the SDK.