Internet-Draft HTMLTrust August 2026
Grey Expires 28 February 2027 [Page]
Workgroup:
Independent Submission
Internet-Draft:
draft-grey-htmltrust-00
Published:
Intended Status:
Experimental
Expires:
Author:
J. Grey
Improbability Engineers, LLC

HTMLTrust: Cryptographically Signed Sections of HTML

Abstract

This document specifies the on-the-wire protocol elements of HTMLTrust: a canonicalization algorithm for HTML text regions, a deterministic signing payload, encodings for hashes and signatures, an endorsement format, and an optional HTTP API for federated trust directories. Together these allow authors to cryptographically attest authorship of arbitrary semantic regions of an HTML document and allow verifiers to confirm those attestations without relying on any privileged authority.

This document defines only the wire protocol. The HTML element through which HTMLTrust is exposed in browsers, its DOM interface, and the user-agent processing model are defined in a companion W3C Community Group Report, referenced here as [HTMLTRUST-W3C], and are out of scope for this document.

About This Document

This note is to be removed before publishing as an RFC.

The latest revision of this draft can be found at https://htmltrust.org/spec/. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-grey-htmltrust/.

Source for this draft and an issue tracker can be found at https://github.com/jasongrey/htmltrust-spec.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 28 February 2027.

Table of Contents

1. Introduction

Transport-layer security on the Web certifies the origin that served a response; it does not certify the author of any particular passage within that response. As large-scale republication and machine-generated content have become routine, readers, downstream systems, and aggregators increasingly lack a reliable means to determine who stands behind a particular passage of text on a page, or whether that text has been altered since publication.

HTMLTrust addresses this gap with a single, narrowly-scoped mechanism: a cryptographic signature, carried in band, over the canonicalized text of a region of an HTML document, bound to its signed publication location and to a resolvable signer identifier. The location is either one exact URL or one HTTPS origin under an explicit signer-selected scope. Verifiers re-derive the canonical text, recompute the hash, resolve the signer's public key, and verify the signature, all without any required network call to a trusted third party.

A companion Community Group Report [HTMLTRUST-W3C] defines the HTML element through which signed regions are expressed in the document, its DOM interface, and the user-agent processing model. This document defines only the protocol artifacts that an interoperable implementation must agree on: the canonicalization algorithm, the signing payload, the encoding of hashes and signatures, the endorsement document format, and the optional HTTP API exposed by a federated trust directory.

1.1. Scope

This document specifies:

  • The canonicalization algorithm that transforms an HTML subtree into a byte string suitable for hashing (Section 4).

  • The signing payload binding (Section 5).

  • The encoding of hashes and signatures on the wire (Section 6).

  • An IANA-style registry of signature and hash algorithms (Section 7).

  • Key resolution mechanisms for the signer identifier (Section 8).

  • The HTTP API exposed by a federated trust directory (Section 10).

  • The endorsement document format (Section 11).

  • The verification procedure that a verifier MUST follow (Section 12).

This document does not specify the HTML element, its DOM interface, parsing behavior, rendering, user-agent UI, or accessibility considerations. Those are defined in [HTMLTRUST-W3C].

1.3. Conventions and Definitions

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

2. Terminology

The following terms are used throughout this document.

Signer:

An entity that produces a signature over a signed section. A signer is identified by a key identifier (see "key identifier") that resolves to a public key.

Verifier:

An entity that consumes a signed section, re-derives the canonical representation, resolves the signer's public key, and validates the signature. The reference verifier is typically a user agent, but command-line tools, search-engine crawlers, and aggregators are also conforming verifiers.

Signed section:

A region of an HTML document that has been signed under this specification. The HTML element that delimits a signed section is the HTMLTrust signed section element as defined in [HTMLTRUST-W3C]. Within this document, references to "the signed section" refer to the abstract region of content and its associated attributes, not to any particular parser representation.

Canonical content:

The deterministic UTF-8 byte string produced by applying the algorithm in Section 4 to the signed text and signed semantic attributes of a signed section.

Canonical claims:

The deterministic UTF-8 byte string produced by applying the algorithm in Section 4.6 to the in-band claim metadata of a signed section.

Content hash:

The cryptographic hash of the canonical content, prefixed with a hash algorithm identifier per Section 6.

Claims hash:

The cryptographic hash of the canonical claims, prefixed with a hash algorithm identifier per Section 6.

Signing payload:

The deterministic byte string defined in Section 5 over which the signature is computed.

Key identifier:

An opaque string ("keyid") that identifies a signer. A key identifier is resolved to a public key by one of the methods in Section 8.

Trust directory:

An optional federated service that indexes signed content, stores endorsements, serves public keys, and may produce reputation scores. The HTTP API is defined in Section 10.

Endorsement:

A signed JSON document attesting that an endorser holds an opinion about a specific content hash at a specific point in time. The format is defined in Section 11.

Origin:

A Web tuple of scheme, host, and port as defined by [RFC6454]. Origin scope uses the URL Standard serialization of this tuple.

Location:

The URL or origin string derived from the final response URL under the signed scope. The location is a member of the v1 signing object.

Signing profile:

A closed, versioned suite that selects the signing object, canonicalization rules, signed attributes, URL policy, scope semantics, and timestamp syntax. This document defines htmltrust-signature-v1.

Signature scope:

The rule that derives the signed location from the final response URL. V1 defines exact URL scope and origin-wide scope.

3. Architecture Overview

The HTMLTrust protocol involves four actors:

Signer:

Produces canonicalized content, computes hashes, constructs the signing payload, and signs it with a private key. Typically embedded in a publishing pipeline (content management system, static-site generator, authoring tool).

Publisher origin:

Serves the resulting HTML to verifiers over the Web. The publisher location is bound into the signing object under the selected scope (Section 5). The publisher origin and the signer MAY be different entities; TLS authenticates transport while the signer attests authorship.

Verifier:

Performs the procedure in Section 12. The verifier re-derives canonical content, recomputes hashes, resolves the signer's public key, and validates the signature. Verification is local except for key resolution, which MAY require a network fetch.

Trust directory:

Optional. Indexes content hashes for discovery, stores endorsements, serves public keys for signers who choose directory-based identity, and MAY compute reputation scores. No directory is privileged by the protocol. A verifier MAY perform full cryptographic verification without ever contacting a directory.

The basic data flow is:

  1. The signer canonicalizes a content region per Section 4 and computes the content hash.

  2. The signer collects in-band claim metadata, canonicalizes the claims per Section 4.6, and computes the claims hash.

  3. The signer constructs the signing payload per Section 5 and signs it with the private key corresponding to the signer's key identifier.

  4. The signer emits the HTML region annotated with the six required v1 attributes defined in Section 5.1 and [HTMLTRUST-W3C].

  5. The publisher origin serves the resulting HTML.

  6. A verifier obtains the HTML, extracts the signed section's attributes and content, and performs the procedure in Section 12.

  7. The verifier MAY consult a trust directory for endorsements, reputation, or key resolution, depending on configuration.

4. Canonicalization

This section defines the deterministic transformation from an HTML subtree to a UTF-8 byte string suitable for hashing. A conforming implementation MUST produce byte-for-byte identical output for inputs that this section declares equivalent.

4.1. Inputs

The input to canonicalization is a DOM subtree rooted at the signed section element. For the purposes of this document, the subtree is treated as the abstract tree defined by the HTML Living Standard parser applied to the source octets. A verifier MUST use that parser model, or an implementation with the same tokenization, tree-construction, and character-reference behavior. A live DOM is suitable only when it is the parser snapshot before script-driven mutation; a post-mutation DOM is a different verification input and MUST be reported as such by the W3C processing model.

A verifier that operates on bytes (for example, a crawler verifying without instantiating a DOM) MUST produce the same result as a DOM-aware verifier on the same source octets.

The signed document URL is the final response URL after redirects. The base URL used to resolve signed href and src values is the document base URL computed by the HTML Standard from the accepted source snapshot, with the signed document URL as its fallback base URL. Runtime mutation of the document URL or a base element MUST NOT change the base URL of an existing source-snapshot result. A verifier operating outside a browser MUST receive the signed document URL and MUST apply the same HTML base-URL algorithm. The location in the signing object is derived from the signed document URL, not from the document base URL.

Portable-profile validation is performed on the source octets and parser diagnostics before the recovered DOM tree is accepted as verification input. A verifier MUST retain enough tokenizer and tree-construction diagnostics to detect the rejected cases in Section 4.1.1. A DOM that has already discarded duplicate attributes or repaired malformed markup is insufficient by itself. If the source octets are unavailable, verification MUST fail with "source-refetch-failed". If the verifier cannot produce the required parser diagnostics from available source, it MUST fail with "parser-profile-unsupported". It MUST NOT treat a live or reconstructed DOM as portable input.

In particular, HTML character references are resolved by the parse model before text normalization (Section 4.4). A byte-oriented implementation MUST decode the full set of HTML5 named character references, matched case-sensitively (Ω and ω are distinct). This document requires character references in signed content to be semicolon-terminated and does not define canonical behavior for the legacy unterminated forms. Numeric character references are decoded using the HTML5 rules: a value of zero, a value greater than U+10FFFF, or a surrogate code point (U+D800..U+DFFF) becomes U+FFFD, and a C1 control in the range U+0080..U+009F maps through the standard windows-1252 replacement table. The abbreviated "common entity" tables shipped by some early implementations are NOT conforming.

4.1.1. Portable parser profile

To make independent implementations interoperable, this revision defines a portable input profile. Every signed section conforming to this revision MUST satisfy this profile. This revision defines no in-band profile negotiation. A signed section MUST be encoded as UTF-8, contain no duplicate attributes, and contain no parse errors involving unclosed or misnested elements, table foster parenting, foreign-content integration points, ambiguous character references, or malformed HTML comments. Outside raw-text elements, an HTML comment in the signed input MUST have a closing -->; its body MUST NOT contain -- and MUST NOT end in -. An unclosed comment, a comment body containing --, or a comment body ending in - is a parse error for this profile. The profile excludes template and nested browsing contexts from the signed subtree as specified in Section 4.3.1. A parser-backed verifier MUST reject an input outside this profile with "parser-profile-unsupported" rather than guessing at a tree. A signer MAY emit other HTML under an experimental protocol version outside this revision, but MUST NOT identify that output as conforming to this revision.

The profile cases in vectors/parser-profile.json cover duplicate attributes, malformed nesting, table insertion, foreign content, ambiguous character references, and malformed comments. A conforming verifier MUST produce the specified accept/reject result before accepting a signed section.

4.2. Walk and text extraction

The canonicalizer performs a depth-first, document-order traversal of the subtree. At each node:

  • If the node is a Text node, its data contributes to the canonical content per Section 4.4.

  • If the node is an Element, the canonicalizer:

    1. Determines whether the element is included, excluded, or boundary- producing per Section 4.3.

    2. If included, emits any signed semantic attribute records for the element per Section 4.3.2.

    3. If included, recurses into its children.

    4. If the element produces a block boundary (Section 4.5), emits the boundary marker after the recursion result.

  • If the node is a Comment, ProcessingInstruction, or DocumentType, it is ignored and contributes nothing.

A signed section element MAY itself be a descendant of another signed section element. The inner element and its subtree are canonicalized independently; from the outer element's perspective, the inner element contributes its canonical content as if it were a transparent block- producing element.

4.3. Element categories

Elements are partitioned into three categories.

4.3.1. Excluded elements

The following elements and their entire subtrees MUST be excluded from the canonical content. Their contents do not contribute any bytes.

  • script

  • style

  • template

  • noscript

  • iframe

  • head

  • link

  • meta

  • HTML comments and processing instructions (already excluded by 4.2).

<meta> elements are excluded from the canonical content in all positions: direct-child claim <meta> elements (Section 4.6) contribute to the canonical claims instead, and any other <meta> contributes nothing. <head> and <link> are document-metadata elements that do not carry signed body content and likewise contribute no bytes; in particular a <link>'s href is NOT a signed semantic attribute.

4.3.2. Included elements

All elements not in Section 4.3.1 are included. Their start and end tags do not themselves contribute bytes, but they MAY contribute block boundaries per Section 4.5 and their descendant text nodes contribute to the canonical content.

Under the htmltrust-attrs-v1 profile defined in Section 5, the following signed semantic attributes also contribute to the canonical content when present on an included element:

This signed-attribute list is fixed for htmltrust-attrs-v1. A future revision MAY define another list under a new profile identifier, but a verifier MUST NOT add attributes to or remove attributes from this profile.

For each included element, before visiting the element's children, the canonicalizer examines the signed semantic attributes in the order listed above. For each present attribute, it appends one attribute record to the canonical content:

@attr ":" element-local-name ":" attribute-name ":" escaped-value "\n"

Immediately before an attribute record is appended, if the canonical output is non-empty and its final byte is neither U+0020 SPACE nor U+000A LINE FEED, the canonicalizer appends one U+000A. Each attribute record already ends in U+000A, so consecutive attribute records need no additional separator.

element-local-name and attribute-name are ASCII-lowercase names as exposed by the HTML parser. normalized-value for alt and aria-label is produced by applying the plain-text normalization in Section 4.4. normalized-value for href and src is produced by applying htmltrust-safe-url-v1 in Section 5.2, then serializing the resulting URL with the Web URL serializer. A URL parsing failure produces "attribute-canonicalization-failed"; a parsed URL outside the selected policy produces "url-policy-violation". The normalized value MUST NOT contain U+000A; if a canonicalizer cannot guarantee this, verification MUST fail with "attribute-canonicalization-failed". escaped-value is formed from normalized-value by replacing every U+0040 COMMERCIAL AT with two consecutive U+0040 bytes. The same replacement is applied to normalized text nodes immediately before they are emitted. This domain-separates attribute records from text records, so literal text cannot emit the reserved record prefix @attr:. The element and attribute names are drawn from fixed ASCII name sets and do not require escaping.

For example, a text node containing the literal string @attr:a:href:x contributes @@attr:a:href:x, while a real href attribute contributes @attr:a:href:x. The two byte sequences therefore remain distinguishable.

Candidate future profiles may cover title, cite, image dimensions, or additional ARIA attributes. Those attributes are outside htmltrust-attrs-v1 and do not contribute bytes under this revision.

4.3.3. Boundary-producing elements

Block-level elements introduce a paragraph boundary in the canonical content (see Section 4.5). The set of boundary-producing element names is exactly:

address, article, aside, blockquote, details, dialog, div, dl, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, li, main, nav, ol, p, pre, section, signed-section, table, td, th, tr, ul.

The element br introduces a soft line break (see Section 4.5).

4.4. Text normalization

For each Text node included in the walk, its data is normalized as follows, in order:

  1. Apply Unicode Normalization Form NFKC ([UNICODE-NFC]).

  2. Strip the formatting characters defined in Section 4.4.2.

  3. Apply the whitespace mapping defined in Section 4.4.3.

  4. Apply the punctuation normalizations defined in Section 4.4.4.

4.4.1. Rationale

These normalizations make the canonical form stable under the silent transformations introduced by content management systems, rich-text editors, and copy-paste operations across applications (curly versus straight quotes, em dash versus double hyphen, fullwidth versus halfwidth forms, and similar). A signer's content remains verifiable after any such round-trip provided the abstract text is unchanged.

4.4.2. Stripped formatting characters

The following characters MUST be removed entirely. They carry no content semantics in any language.

  • U+00AD SOFT HYPHEN

  • U+200B ZERO WIDTH SPACE

  • U+200E LEFT-TO-RIGHT MARK

  • U+200F RIGHT-TO-LEFT MARK

  • U+2060 WORD JOINER

  • U+FEFF BYTE ORDER MARK / ZERO WIDTH NO-BREAK SPACE

  • U+034F COMBINING GRAPHEME JOINER

  • U+061C ARABIC LETTER MARK

  • U+180E MONGOLIAN VOWEL SEPARATOR

  • U+0640 ARABIC TATWEEL

  • U+FE00..U+FE0F VARIATION SELECTORS 1-16

  • U+E0100..U+E01EF VARIATION SELECTORS 17-256

  • U+E0001..U+E007F TAG CHARACTERS

  • U+202A..U+202E BIDI EMBEDDING CONTROLS

  • U+2066..U+2069 BIDI ISOLATE CONTROLS

  • U+2061..U+2064 INVISIBLE MATH OPERATORS

  • U+FFF9..U+FFFC INTERLINEAR ANNOTATION ANCHORS AND OBJECT REPLACEMENT

The following characters MUST be preserved despite their visual invisibility. They are semantically significant in major writing systems.

  • U+200C ZERO WIDTH NON-JOINER (semantic in Persian, Kurdish, Syriac).

  • U+200D ZERO WIDTH JOINER (semantic in Indic scripts and emoji sequences).

4.4.3. Whitespace

Outside of a <pre> element, each of the following Unicode code points MUST be replaced with U+0020 SPACE:

U+0009, U+000A, U+000B, U+000C, U+000D, U+0085, U+00A0, U+1680, U+2000..U+200A, U+2028, U+2029, U+202F, U+205F, U+3000.

After replacement, runs of two or more U+0020 SPACE characters MUST be collapsed to a single U+0020 SPACE. Leading and trailing whitespace within each block (delimited by Section 4.5 boundaries) MUST be removed.

In this revision, <pre> receives no special whitespace treatment: it is a boundary-producing block (Section 4.3.3) and the text within it is normalized identically to all other content, including the whitespace mapping and collapsing above. Verbatim whitespace preservation inside <pre> (so that indentation in code samples is bound byte-for-byte) is deliberately deferred to a future revision: at the time of writing, no byte-identical implementation of per-element preservation across the independent HTML parsers used by conforming implementations was available, and shipping an under-specified rule here is precisely the interoperability failure this document exists to avoid. A signer MUST NOT rely on whitespace inside <pre> being cryptographically bound in this revision. This is consistent with the deliberate semantic-subset signing trade-off (see Security Considerations).

4.4.4. Punctuation normalization

The following character classes are normalized to ASCII. The intent is hash stability across CMS-driven typographic transformations.

Single quotation marks: each of U+2018, U+2019, U+201A, U+201B, U+2032, U+2039, U+203A, U+0060, U+00B4 MUST be replaced with U+0027.

Double quotation marks: each of U+201C, U+201D, U+201E, U+201F, U+2033, U+00AB, U+00BB, U+300C, U+300D, U+300E, U+300F, U+301D..U+301F, U+FE41..U+FE44 MUST be replaced with U+0022.

Dashes and hyphens: each of U+2010, U+2011, U+2012, U+2013, U+2014, U+2015, U+2212, U+FE58, U+FE63 MUST be replaced with U+002D.

Ellipsis: each of U+2026, U+FE19 MUST be replaced with the three-byte sequence U+002E U+002E U+002E.

4.5. Block structure

A boundary-producing element (Section 4.3.3) emits a single U+000A LINE FEED character after its descendants have contributed their text. The br element emits a single U+000A LINE FEED character at its position.

After the entire subtree has been walked, the resulting byte sequence is trimmed: leading and trailing runs of U+000A and U+0020 are removed, and runs of two or more U+000A in a row are collapsed to a single U+000A.

The result is a UTF-8 byte string.

4.6. Canonical claims

In-band claim metadata is carried by <meta> elements that appear as direct children of the signed section element, as required by [HTMLTRUST-W3C]. Claim <meta> elements located at greater depth do not participate.

Every direct child <meta> element of a signed section is a claim. A claim <meta> element MUST have both a name attribute and a content attribute. If any direct child claim <meta> is missing either attribute, verification MUST fail with "claim-malformed". If the normalized name is the empty string, verification MUST fail with "claim-malformed".

For each claim <meta> element:

  1. The name attribute value is processed as plain text per Section 4.4 (NFKC, formatting strip, whitespace collapse).

  2. The content attribute value is processed identically.

  3. The pair is serialized as

    escaped-name : escaped-content \n

    where : is U+003A COLON and \n is U+000A LINE FEED. Escaping is applied after normalization: U+005C REVERSE SOLIDUS becomes \\, U+003A COLON becomes \:, and U+000A LINE FEED becomes \n. Normalization currently maps line feeds to spaces, but the explicit rule keeps the record grammar injective if a future normalization profile admits them.

The resulting name : content lines are sorted lexically by the UTF-8 byte sequence of the normalized name. Ties on name MUST NOT occur; signers MUST NOT emit two direct child claim <meta> elements whose name attributes normalize to the same value within a single signed section, and verifiers MUST fail with "claim-duplicate" if they encounter such duplicates. The concatenation of the sorted lines is the canonical claims byte string.

Normalized claim names are case-sensitive. Only the exact ASCII name signed-at has protocol semantics in v1. Other names, including author and names in the claim: namespace, are signed metadata whose meaning is defined outside the cryptographic verification procedure.

The signed-at claim is not special for purposes of the claims hash: it is included in canonical claims like every other direct child claim <meta>. Section 5 also includes the normalized signed-at value as a separate signing-payload field. This deliberate duplicate binding makes the timestamp easy for verifiers and directory indexes to extract without weakening claim integrity.

4.7. Opt-out marker (Editor's Note)

Editor's Note: Common runtime decorations (copy-to-clipboard buttons, client-side syntax highlighting, lazy-loaded share widgets) frequently mutate descendants of a signed section after page load. This revision addresses that problem by verifying the original server HTML snapshot in the companion W3C processing model. A future revision might add an author-controlled opt-out marker such as data-htmltrust-ignore="true", but that attribute has no normative effect in this revision. Conforming canonicalizers MUST NOT exclude a subtree solely because it carries that attribute unless operating in an explicit experimental mode.

5. Signing Payload Binding

This section defines the htmltrust-signature-v1 signing profile. The profile is a closed suite. It fixes the signing-payload representation, canonicalization profile, signed-attribute set, URL policy, location scope semantics, and timestamp syntax. A later revision that changes any of those rules MUST use a different signing-profile identifier.

5.1. Profile identifiers and wire attributes

The identifiers fixed by this profile are:

Table 1
Purpose Identifier
signing profile htmltrust-signature-v1
canonicalization profile htmltrust-c14n-v1
signed-attribute profile htmltrust-attrs-v1
URL policy profile htmltrust-safe-url-v1

The identifiers are case-sensitive ASCII strings. They are protocol identifiers, not implementation version numbers.

A v1 signed section MUST carry the profile attribute with the exact value htmltrust-signature-v1 and the signature-scope attribute with one of the exact values url or origin, in addition to keyid, signature, content-hash, and algorithm. Leading or trailing ASCII whitespace in any of these protocol attributes is invalid. There are no default values.

A missing required attribute produces incomplete. An unknown profile produces profile-unsupported, and an unknown signature-scope produces scope-unsupported. A verifier MUST select the profile before canonicalization and MUST NOT retry a failed v1 verification with another profile.

5.2. Signed-attribute and safe-URL profiles

htmltrust-attrs-v1 contains exactly href, src, alt, and aria-label in the order specified in Section 4.3.2. Adding, removing, reordering, or changing the interpretation of a covered attribute requires a new signed-attribute profile identifier. Implementations MUST NOT extend this set while reporting htmltrust-attrs-v1.

For href and src, htmltrust-safe-url-v1 applies before the serialized URL is emitted into canonical content. Its input is the attribute value produced by the accepted HTML parser after input-stream preprocessing and character-reference resolution. Before passing that value to the URL Standard parser, the verifier MUST inspect every code point and reject an ASCII C0 control or U+007F. This order prevents URL preprocessing from silently stripping a tab or line feed. The value is resolved against the source snapshot's document base URL. The result MUST use the https scheme and MUST NOT contain a username or password. Query and fragment components are preserved by URL serialization. Verification does not dereference the URL. A policy violation produces url-policy-violation; a URL parse failure produces attribute-canonicalization-failed.

This profile therefore rejects javascript, data, blob, file, mailto, tel, custom schemes, and cleartext http in signed href and src attributes. An implementation MAY define a local-development profile, but it MUST use a different URL-policy identifier and MUST NOT be accepted as htmltrust-safe-url-v1.

The profile does not cover srcset, CSS URLs, event-handler attributes, or attributes outside htmltrust-attrs-v1. User interfaces MUST describe the result as verification of the covered content and attributes rather than as whole-document integrity.

5.3. Location scope and same-origin replay

The signed document URL is the final response URL after redirects. Public v1 signatures require a URL with an HTTPS tuple origin and no username or password. Host case folding, IDNA A-label conversion, IPv6 syntax, and default-port omission follow the URL Standard.

For signature-scope="url", the location is the URL Standard serialization of the signed document URL with its fragment excluded. The path and query are included. A fragment is excluded because it is not sent in an HTTP request. This scope prevents an unchanged section from verifying at a different path or query on the same origin. Authoring tools SHOULD emit url scope.

For signature-scope="origin", the location is the URL Standard origin serialization of the signed document URL. This value contains scheme, host, and non-default port, with no path, query, fragment, or credentials. Origin scope explicitly permits replay at another URL on the same origin. Verifiers MUST expose the accepted scope to trust policy and user-interface code so a caller can reject origin-wide signatures.

The document's <link rel="canonical">, Open Graph URL, document base URL, and surrounding markup do not select or alter the location. Copying a section to another origin always changes the location. www and apex hosts, CDN aliases, and distinct ports are distinct locations unless they serialize to the same origin under the URL Standard. A URL that cannot produce the required public location produces origin-not-supported.

5.4. Timestamp profile

Every v1 section MUST contain exactly one direct-child claim whose normalized name is the ASCII string signed-at. Its normalized content MUST be exactly 20 ASCII characters in this form:

YYYY-MM-DDTHH:MM:SSZ

The year is in the range 0001 through 9999. The month, day, hour, minute, and second MUST form a valid Gregorian UTC date and time. Seconds are limited to 00 through 59. Lowercase t or z, UTC offsets, fractional seconds, leap seconds, and trailing whitespace are invalid. This is a deliberately narrow profile of [RFC3339]. A violation produces timestamp-invalid.

signed-at is an assertion made by the signer. It proves that the timestamp was covered by the signature, but it does not prove when the signature was created. Freshness limits and future-clock-skew rules are trust-policy decisions and MUST NOT be reported as signature-invalid. A verifier that accepts a signature from a revoked key based on time needs independent evidence, such as an [RFC3161] time-stamp token, an append-only log entry, or a trusted directory first-seen record. Such evidence is evaluated outside the v1 signing payload.

5.5. Canonical signing object

The signer constructs this JSON object, with every member present:

{
  "algorithm": "ed25519",
  "attributeProfile": "htmltrust-attrs-v1",
  "canonicalizationProfile": "htmltrust-c14n-v1",
  "claimsHash": "sha256:...",
  "contentHash": "sha256:...",
  "context": "https://htmltrust.org/protocol/signed-section",
  "keyid": "https://keys.example/alice.json",
  "location": "https://example.org/article",
  "profile": "htmltrust-signature-v1",
  "scope": "url",
  "signedAt": "2026-08-27T18:00:00Z",
  "urlProfile": "htmltrust-safe-url-v1"
}

The members are defined as follows:

algorithm:

The exact algorithm attribute value. The value MUST be a registered algorithm identifier accepted by the verifier.

attributeProfile, canonicalizationProfile, profile, urlProfile:

The exact identifiers in Section 5.1. These values are constants for v1 and MUST NOT be copied from attacker-selected markup.

claimsHash:

The claims hash recomputed from Section 4.6 using the algorithm selected by contentHash. It is not carried as a separate HTML attribute. The content and claims hash algorithms MUST match.

contentHash:

The exact content-hash attribute value after its encoding has been validated as canonical under Section 6.

context:

The exact ASCII string https://htmltrust.org/protocol/signed-section. This member separates HTMLTrust signed-section signatures from signatures made for another protocol.

keyid:

The exact keyid attribute value after the protocol-attribute validation in Section 5.1. It selects one verification key under Section 8.

location:

The location derived by Section 5.3 from the accepted source snapshot's final response URL and the selected scope.

scope:

The exact signature-scope attribute value.

signedAt:

The normalized signed-at claim content after Section 5.4 validation.

The signing payload is the UTF-8 encoding of this object serialized with the JSON Canonicalization Scheme [RFC8785]. The serialized object contains no unknown or optional members in v1. The signature operation in Section 6.3 is applied directly to those bytes.

5.6. Algorithm and signer binding

Both algorithm and keyid are inside the signed object. Changing either attribute changes the signing payload and causes signature verification to fail. The resolved key document MUST identify a key suitable for exactly the declared algorithm; a verifier MUST NOT try other algorithms or other keys after a mismatch.

A public-key fingerprint is not a v1 payload member. keyid selects one verification method, and the signature already proves possession of the corresponding private key. Key rotation uses a fresh keyid under Section 9. Implementations MAY expose a derived key fingerprint for diagnostics or trust policy, but it does not replace signed keyid binding.

5.7. Legacy payloads

The four-field colon-joined payload used by pre-v1 prototypes is not part of htmltrust-signature-v1. An implementation MAY offer an explicitly enabled legacy verifier, but absence of profile MUST NOT trigger it automatically. A legacy success MUST be labeled separately and MUST NOT be reported as a v1 valid result. Migration consists of verifying the legacy artifact under its original rules, canonicalizing the accepted source under v1, and producing a new v1 signature. Re-encoding a legacy signature does not migrate it.

6. Hash and Signature Encoding

6.1. Encoding choice

Binary values on the wire, including hashes, signatures, and raw public keys embedded in HTMLTrust JSON documents, MUST be encoded as canonical, unpadded Base64 using the standard Base64 alphabet in Section 4 of [RFC4648]. This is not base64url: the alphabet includes + and /, and does not use - or _.

Producers MUST emit the shortest canonical unpadded Base64 form:

  • no = padding characters;

  • no whitespace;

  • only the characters A-Z, a-z, 0-9, +, and /;

  • zero-valued unused pad bits as required by [RFC4648].

Verifiers MUST reject non-canonical Base64 in signed attributes and signed JSON fields. A verifier determines canonicality by decoding the value, re-encoding the decoded bytes using the rules above, and requiring byte-for-byte equality with the original value. Implementations MAY provide an explicit legacy-tooling mode that accepts padded Base64 or base64url for migration testing, but that mode is non-conforming for normal verification and MUST be distinguishable from conforming verification in diagnostics.

Because standard Base64 uses + and /, HTMLTrust values placed in URL paths, query strings, or other URL components MUST be percent-encoded where required by the URL component's grammar. For example, the / character in a content hash path segment is encoded as %2F.

Editor's Note: Hex (familiar from git and TLS tooling) and base32 (case-insensitive and easier to transcribe) remain useful diagnostic formats, but they are not wire encodings for this revision. Ecoji [ECOJI] has been proposed; it has been rejected on wire-byte grounds (32-byte SHA-256 digest is 26 emoji = 104 UTF-8 bytes versus 43 Base64 characters) and is noted here for completeness.

6.2. Content hash and claims hash

A content hash or claims hash is the concatenation of:

  1. A hash algorithm identifier from the registry in Section 7.2.

  2. A single U+003A COLON.

  3. The Base64-encoded, unpadded hash output.

For example, a SHA-256 hash of an empty string is:

sha256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU

Verifiers MUST reject a content hash whose algorithm identifier is not in the registry, and MUST reject a content hash whose Base64-decoded length is not the natural output length of the identified algorithm (32 bytes for sha256, 48 bytes for sha384, 64 bytes for sha512).

6.3. Signature

The signature attribute carries the Base64-encoded, unpadded output of the signature algorithm given by the algorithm attribute. The byte representation is fixed for each registered algorithm:

  • For ed25519, the signature is the 64-byte Ed25519 signature defined by [RFC8032] (86 Base64 characters).

  • For ecdsa-p256, the signature is the 64-byte R || S representation defined for ES256 by [RFC7518]. R and S are unsigned, big-endian, 32-byte integers, left-padded with zero octets when necessary.

  • For ecdsa-p384, the signature is the 96-byte R || S representation defined for ES384 by [RFC7518]. R and S are unsigned, big-endian, 48-byte integers, left-padded with zero octets when necessary.

  • For either RSA algorithm, the signature is the big-endian signature integer encoded in exactly the modulus width in octets, as specified by [RFC8017]. rsa-pss-sha256 uses SHA-256, MGF1 with SHA-256, and a 32-octet salt. rsa-pkcs1-sha256 uses RSASSA-PKCS1-v1_5 with SHA-256.

ECDSA ASN.1 DER signatures are not valid HTMLTrust signature values. Verifiers MUST reject a signature whose decoded length or representation does not match the selected algorithm before attempting verification.

7. Algorithm Registry

This section requests creation of two IANA registries (see Section 13). The initial contents are listed here.

7.1. Signature algorithms

The following identifiers are defined for use in the algorithm attribute of a signed section.

Table 2
Identifier Algorithm Reference
ed25519 EdDSA over edwards25519 [RFC8032]
ecdsa-p256 ECDSA with SHA-256 over secp256r1 [RFC7518]
ecdsa-p384 ECDSA with SHA-384 over secp384r1 [RFC7518]
rsa-pss-sha256 RSASSA-PSS with SHA-256, MGF1-SHA-256, and a 32-octet salt [RFC8017]
rsa-pkcs1-sha256 RSASSA-PKCS1-v1_5 with SHA-256 [RFC8017]

The mandatory-to-implement algorithm for both signers and verifiers is ed25519. A verifier MAY accept additional algorithms; a verifier MUST treat an algorithm value not in its accepted set as an "algorithm-not-supported" verification failure, not as a generic failure.

The resolved public key type and parameters MUST match the selected identifier exactly. In particular, an ECDSA key on another curve and a key document that names the other registered RSA padding mode are algorithm mismatches. A verifier MUST NOT infer an algorithm from key type or try multiple algorithms after a mismatch.

7.2. Hash algorithms

The following identifiers are defined for use in the algorithm prefix of content-hash and claims-hash.

Table 3
Identifier Algorithm Output length
sha256 SHA-256 32 bytes
sha384 SHA-384 48 bytes
sha512 SHA-512 64 bytes

The mandatory-to-implement hash algorithm is sha256. The hash algorithm used for content-hash and claims-hash within a single signed section MUST be the same.

7.3. Algorithm agility

A future revision of this specification MAY add identifiers via expert review (Section 15). Identifiers MUST NOT be removed once registered; an identifier MAY be marked "deprecated" with normative text recommending non-use. Verifiers SHOULD refuse to verify under a deprecated identifier and SHOULD surface the result as an "algorithm-not-supported" outcome.

8. Key Resolution

The keyid attribute of a signed section is opaque to the protocol. This section defines three resolution methods. A verifier MUST implement at least one and SHOULD implement all three; a signer SHOULD use the most decentralized method available in its operating context.

8.1. DID method resolution

A keyid whose value begins with did: MUST be resolved per the DID Core specification [W3C.did-core] using the DID method indicated by the second URI scheme component. The DID document MUST be retrieved, and the public key MUST be selected from its verificationMethod array.

The verifier MUST select a verification method whose type is compatible with the algorithm attribute of the signed section. If multiple verification methods are present, the verifier MUST prefer a method whose id is referenced from the assertionMethod relationship of the DID document; if no such method exists, the verifier MAY use any verification method of compatible type.

DID resolution failures (DID document not found, signature on DID document invalid where the method demands one, expired or revoked verification method) MUST be reported as "key-resolution-failed" verification failures.

8.2. Direct HTTPS URL

A keyid whose value is an absolute URL with scheme https MUST be resolved by issuing a GET request to that URL. The response MUST have a media type of application/htmltrust-key+json (Section 15) or application/jwk+json per [RFC7517]; verifiers MAY accept application/json for backward compatibility.

The retrieved document MUST be one of:

  • A JSON Web Key per [RFC7517], or

  • An HTMLTrust key document with the following shape:

    {
      "kid": "<string, optional>",
      "algorithm": "<algorithm identifier>",
      "publicKeyEncoding": "spki-der",
      "publicKey": "<unpadded standard Base64 SPKI DER>",
      "expires": "<RFC3339 timestamp, optional>",
      "revoked": <boolean, optional>,
      "revokedAt": "<RFC3339 timestamp, optional>",
      "supersededBy": "<keyid, optional>",
      "previousKeys": ["<keyid>"]
    }
    

algorithm, publicKeyEncoding, and publicKey are REQUIRED. The other members shown above are OPTIONAL. Unknown members MUST be ignored for key resolution and MAY be retained by caches or directories.

publicKey is the canonical unpadded standard Base64 encoding from Section 6.1 of the DER-encoded SubjectPublicKeyInfo structure defined by [RFC5280]. publicKeyEncoding MUST be exactly spki-der. A verifier MUST reject any other encoding in a conforming HTMLTrust key document with a "malformed-key-document" result. If kid is present, it MUST equal the serialized URL used as keyid; a mismatch is a "key-resolution-failed" result. The algorithm value and the decoded key type and parameters MUST match Section 7.1.

For an application/jwk+json response, the JWK kty, crv, and alg members, when applicable, MUST select the same key type, parameters, and algorithm. An absent alg does not authorize algorithm inference; the signed section's algorithm still selects the verification operation.

The verifier MUST process expires and revoked according to Section 9. Malformed timestamps, malformed Base64, an invalid SPKI structure, and missing or incorrectly typed required members are "malformed-key-document" results. A well-formed key whose type, curve, or declared algorithm does not match the signed section produces "algorithm-mismatch". A conforming verifier MUST NOT use a PEM compatibility field or silently try a different key encoding.

8.3. Trust directory reference

A keyid whose value is an absolute URL pointing at a trust directory's /keys/{id} endpoint (Section 10.6) MUST be resolved by issuing a GET request to that URL. The response format is identical to Section 8.2.

The verifier SHOULD consider the trustworthiness of the directory itself before accepting a key resolved this way; directory-based identity centralizes a portion of the trust graph in the directory operator.

8.4. Acceptance policy

A verifier MUST permit configuration of which key-resolution methods it accepts and SHOULD default to accepting all three. The verifier MUST NOT privilege any one method over another within its accepted set; selection is determined by the keyid value, not by the verifier.

9. Key Lifecycle

A provenance system that binds durable content to keys must define what happens when keys change over their lifetime. This section is normative for how keys are rotated, expired, revoked, and superseded, and how those states interact with signatures already in the wild. The naive "one boolean revoked flag" model is insufficient because it cannot distinguish an orderly rotation from a compromise, and it destroys the verifiability of an author's entire signed history at once.

9.1. Key states

A published key is in exactly one of four states from a verifier's perspective:

  • active -- neither expired nor revoked; usable for verification.

  • expired -- the key document carries an expires timestamp in the past. The key was retired on schedule. Signatures whose bound signed-at is at or before expires remain cryptographically valid and SHOULD be accepted subject to trust policy; a verifier MUST fail with "key-revoked" if it cannot establish that the signature predates expiry.

  • revoked -- the key document carries revoked: true. Revocation signals suspected or confirmed key compromise. See Revocation, compromise, and signed-at freshness, below.

  • superseded -- the key is expired and the publisher has designated a successor (see Rotation and supersession, below). This is the normal rotation end-state.

9.2. Rotation and supersession

Publishers SHOULD rotate signing keys periodically. To rotate without invalidating existing content, a publisher:

  1. Publishes a new key at a fresh keyid and begins signing new content with it.

  2. Sets an expires timestamp on the old key document at or after the time it stopped signing with that key, and keeps the old key document resolvable so historical signatures continue to verify.

  3. MAY add a supersededBy field to the old key document whose value is the successor keyid, and a previousKeys array to the new key document listing prior keyids. These fields let a verifier or directory reconstruct an author's key history and attribute an author's older content to the same identity across a rotation.

A verifier MUST NOT treat an expired-but-superseded key as a "key-revoked" failure for signatures whose signed-at precedes the expires time; expiry alone is orderly rotation, not compromise.

9.3. Revocation, compromise, and signed-at freshness

signed-at (Section 5) is asserted by the signer and, absent an external timestamp authority, can be backdated by whoever holds the private key. This has a direct consequence for compromise handling: a holder of a stolen key can produce signatures bearing any signed-at value, including one predating the compromise. Therefore:

  • A revoked key document SHOULD carry a revokedAt RFC 3339 timestamp indicating when compromise is believed to have begun.

  • A conservative verifier MUST treat every signature from a revoked key as untrusted, regardless of its signed-at, because the timestamp is not independently attestable. This is the safe default.

  • A verifier MAY accept a signature from a revoked key whose signed-at precedes revokedAt ONLY when the signature's timing is corroborated by evidence the verifier trusts and that the key holder could not forge -- for example an [RFC3161] time-stamp token, inclusion in an append-only transparency log, or a directory's first-seen record established before revokedAt. In the absence of such evidence, revocation is all-or-nothing.

Verifiers MAY additionally apply a freshness policy that rejects signed-at values implausibly far in the past or future relative to the time of retrieval; such a policy is a trust-layer decision (out of scope for cryptographic verification) and is not part of signature verification.

9.4. Key loss and recovery

There is no cryptographic recovery from loss of a signing key: the private key is the identity. A publisher that loses a key (without compromise) SHOULD mark it expired, publish a successor (see Rotation and supersession), and rely on the out-of-band binding between keyid and publisher (the did:web/HTTPS origin, or directory registration and endorsements) to re-establish continuity of identity. Publishers SHOULD therefore keep signing keys recoverable from secure backups precisely because the protocol offers no in-band recovery. Directories and endorsers MAY vouch for a successor key's continuity with the prior identity, but such vouching is a trust-layer signal, not a cryptographic guarantee.

10. Trust Directory HTTP API

This section defines the HTTP API exposed by an optional federated trust directory. A directory implementing this API conforms to [RFC9110] and supports content negotiation and standard cache directives [RFC9111].

The API shape in this section is normative for conforming trust directories. Reference implementations and deployed services that diverge from these endpoints, field names, media types, or verification requirements need to change to match this specification rather than the reverse.

A verifier MAY consult zero, one, or multiple directories. The protocol does not require directory consultation for cryptographic verification (Section 12.1 through 12.6); only endorsement and reputation use cases require directory access.

10.1. Base URL and discovery

A directory MUST be reachable at an https-scheme base URL. The endpoints below are relative to that base URL.

A directory SHOULD also expose a discovery document at the well-known URI [RFC8615] /.well-known/htmltrust (Section 10.2).

10.2. GET /.well-known/htmltrust

Returns a JSON metadata document describing the directory's capabilities. The response media type is application/htmltrust-directory+json (provisional; see Section 15).

{
  "directory": "https://directory.example/",
  "version": "1",
  "capabilities": {
    "content": true,
    "endorsements": true,
    "keys": true,
    "reputation": true
  },
  "supportedAlgorithms": {
    "signature": ["ed25519", "ecdsa-p256"],
    "hash": ["sha256", "sha384"]
  },
  "supportedProfiles": ["htmltrust-signature-v1"],
  "contact": "operator@directory.example",
  "termsOfService": "https://directory.example/tos"
}

The fields directory, version, capabilities, supportedAlgorithms, and supportedProfiles are REQUIRED. Other fields are OPTIONAL.

10.3. GET /content/{hash}

Retrieves the directory's record for a specific content hash. The path parameter is the content hash including the algorithm prefix, percent-encoded as a single path segment (the colon becomes %3A; any / in the Base64 value becomes %2F).

A successful response is JSON of the form:

{
  "contentHash": "sha256:Zm9vYmFy...",
  "firstSeen": "2026-05-15T12:34:56Z",
  "signers": [
    {
      "profile": "htmltrust-signature-v1",
      "keyid": "did:web:author.example#key-1",
      "algorithm": "ed25519",
      "signedAt": "2026-05-01T10:30:00Z",
      "scope": "url",
      "location": "https://author.example/posts/123",
      "signature": "3q2+7w8NslfJ..."
    }
  ],
  "endorsementCount": 3
}

A directory that does not have a record for the requested hash MUST return 404 Not Found.

10.4. POST /content

Submits a record of a signed content occurrence for indexing. The request body is JSON of the form:

{
  "profile": "htmltrust-signature-v1",
  "contentHash": "sha256:Zm9vYmFy...",
  "keyid": "did:web:author.example#key-1",
  "algorithm": "ed25519",
  "signedAt": "2026-05-01T10:30:00Z",
  "scope": "url",
  "location": "https://author.example/posts/123",
  "signature": "3q2+7w8NslfJ...",
  "sourceURL": "https://author.example/posts/123",
  "claims": [
    {"name": "author", "content": "Alice Example"},
    {"name": "claim:License", "content": "CC-BY-4.0"},
    {"name": "signed-at", "content": "2026-05-01T10:30:00Z"}
  ]
}

The submission MUST be authenticated by an HTTP Message Signature [RFC9421] from a key the directory can resolve. The directory MUST re-verify the submitted signature against the canonical signing payload (Section 5) before indexing.

The profile, algorithm, keyid, scope, location, contentHash, signedAt, and signature members are REQUIRED. sourceURL is the observed occurrence URL. The directory MUST derive a location from sourceURL under the submitted scope and require it to equal location. A directory MUST NOT derive the signed location from canonical-link metadata.

The claims array MUST contain the complete set of direct-child claim records used by the signer, including exactly one signed-at record. Each array member MUST contain exactly one string name and one string content. The directory MUST normalize, escape, detect duplicates, sort, and serialize these records exactly as specified in Section 4.6. It MUST derive the claims hash with the hash algorithm selected by contentHash, and it MUST require the normalized signed-at claim value to equal signedAt. A missing, extra, malformed, or duplicate claim causes the submission to fail. The directory MUST use this recomputed claims hash when constructing the Section 5 signing payload; it MUST NOT insert a missing claim or accept a client-supplied claims hash in its place.

Successful submission returns 201 Created with a Location header pointing at the resulting /content/{hash} URL. Rejected submissions return 4xx codes per Section 10.9.

10.5. GET /content/{hash}/endorsements

Lists endorsements stored for a given content hash. The response is a JSON array of endorsement documents (Section 11), each independently verifiable by the requester. The directory MUST NOT alter the endorsement payloads in a manner that invalidates the endorser's signature.

Pagination, if supported, MUST follow the Web Linking conventions of [RFC9110] using Link headers with rel="next".

10.6. POST /endorsements

Submits a signed endorsement document (Section 11). The directory MUST:

  1. Verify the endorser's signature on the endorsement.

  2. Verify that the endorsement's endorsement field references a content hash for which the directory has a record (or accept new content hashes; this is a directory policy choice).

  3. Index the endorsement under the content hash for retrieval via Section 10.5.

Successful submission returns 201 Created. Endorsements with invalid signatures MUST be rejected with 400 Bad Request.

10.7. GET /keys/{id}

Retrieves a public key document for the given directory-issued key identifier. The response is a key document conforming to Section 8.2.

Directories that act as convenience registries for less-technical signers expose keys through this endpoint, and signers reference such keys by setting keyid to the full URL of the endpoint, for example https://directory.example/keys/k-abc123.

10.8. GET /signers/{id}/reputation

Returns a directory-computed reputation score for a signer. The response is JSON of the form:

{
  "keyid": "did:web:author.example",
  "score": 0.72,
  "asOf": "2026-05-15T00:00:00Z",
  "components": ["endorsements", "history", "verified-domain"],
  "methodology": "https://directory.example/methodology"
}

The score is a directory-specific value with no protocol-level semantics; verifiers consume scores from directories that they have been explicitly configured to trust. Different directories MAY produce different scores for the same signer.

10.9. Authentication

POST endpoints MUST be authenticated using HTTP Message Signatures [RFC9421] with a key that the directory can resolve via Section 8. The signature input MUST be an RFC 9421 structured field. A conforming directory MUST accept the sig1 label only when its covered-component identifier list includes, in this order, @method, @target-uri, host, date, and content-digest. The legacy (request-target) identifier is an earlier HTTP Signatures convention and MUST NOT be used in a conforming HTMLTrust signature. The @target-uri value MUST be the absolute request target, and host and date MUST be the received HTTP fields. content-digest MUST use the representation from [RFC9530] and MUST cover the exact UTF-8 request body bytes. The signature parameters MUST include created, keyid, and alg; created MUST be within the directory's configured clock-skew window and implementations SHOULD include a nonce to prevent replay of otherwise valid requests.

The alg parameter identifies the HTTP Message Signature algorithm profile. For this revision, ed25519 means an Ed25519 signature over the RFC 9421 signature base, encoded as unpadded standard Base64. It is independent of the HTMLTrust signed-section algorithm field. A directory MUST reject a request whose signature base, body digest, or freshness checks do not verify; API keys MAY be offered as a separately documented deployment convenience, but are not a conforming substitute for this authentication profile.

GET endpoints SHOULD be accessible without authentication to support public verification. A directory MAY require authentication for specific endpoints subject to its operational policy; in that case unauthenticated requests MUST receive 401 Unauthorized with a WWW-Authenticate header.

10.10. Errors

The directory MUST return errors using the problem-details format defined in [RFC9457] (which obsoletes RFC 7807), with media type application/problem+json:

{
  "type": "https://htmltrust.org/errors/signature-invalid",
  "title": "Signature verification failed",
  "status": 400,
  "detail": "The submitted signature did not verify against the
  canonical signing payload.",
  "contentHash": "sha256:..."
}

Directories SHOULD apply rate limits per IP, per submitter key, and per content hash, and SHOULD signal exhaustion with 429 Too Many Requests and a Retry-After header.

11. Endorsement Format

An endorsement is a standalone, signed JSON document attesting to an endorser's opinion of a specific content hash at a specific moment in time. Endorsements target content hashes, not signers; an ongoing opinion of a signer is expressed through directory reputation, not through a persistent signer-level endorsement artifact.

The structured endorsement format defined in this section is normative. Conforming directories MUST store, return, and verify endorsement documents in this shape and MUST NOT replace it with an implementation- specific flat signature record.

11.1. Document shape

The endorsement document is a JSON object with the following fields.

Table 4
Field Type Required Description
endorser string yes Key identifier of the endorser, resolved per Section 8.
endorsement string yes Content hash being endorsed, including algorithm prefix.
signature string yes Base64-encoded signature over the canonicalized document; see Section 11.2.
algorithm string yes Signature algorithm identifier from Section 7.1.
timestamp string yes RFC 3339 UTC timestamp at which the endorsement was issued.
expires string no RFC 3339 UTC timestamp at which the endorsement ceases to be valid.
claim string no Free-text human-readable rationale for the endorsement.
revokedBy string no If present, the endorsement has been superseded by the document with this content hash.

Additional fields MAY be present and SHOULD be preserved by directories. Verifiers MUST ignore fields they do not recognize for purposes of trust decisions, but MUST include them when computing the signed payload (Section 11.2) so that signature verification succeeds.

11.2. Canonicalization for signing

The signing payload for an endorsement is the JSON Canonicalization Scheme [RFC8785] serialization of the endorsement document with the signature field omitted. The endorser signs that byte string using the algorithm given by algorithm. Verifiers reproduce the same serialization, omit signature, and verify using the endorser's resolved public key.

JSON Canonicalization Scheme is chosen because it is purpose-built for signed JSON, is widely implemented, and avoids the parser- divergence problems that have historically plagued ad-hoc JSON canonicalization. Implementations MUST implement RFC 8785 rather than an object-key sorter layered on the host language's ordinary JSON serializer. In particular, they MUST use RFC 8785's UTF-16 property-name ordering, ECMAScript-compatible number serialization, JSON string serialization rules, and no insignificant whitespace. This includes the required escaping of control characters and rejection of lone UTF-16 surrogate code units. JSON parsers MUST reject duplicate member names before JCS serialization; accepting the last duplicate member is non-conforming. Values that JSON cannot represent under RFC 8259, including NaN and Infinity, MUST be rejected. The signature member is omitted only at the signing step; all other members, including optional and implementation-extension members, remain in the JCS input. A verifier MUST preserve and include unknown members rather than silently dropping them.

11.3. Revocation and expiry

An endorsement with an expires value in the past MUST be treated by verifiers as invalid for purposes of any current trust decision. An endorsement with a revokedBy value MUST be treated as superseded; the verifier MAY retrieve the superseding endorsement from the same directory and apply its semantics in place.

An endorser MAY publish a revocation endorsement (an endorsement whose claim field names the revoked endorsement's hash and whose revokedBy field references the document being revoked). A directory that holds both MUST serve both in response to GET /content/{hash}/endorsements so that verifiers can observe the revocation chain.

11.4. Endorsement of an endorsement

Endorsement documents are themselves content. An endorser MAY in turn endorse another endorser's document by computing the content hash of the endorsement (as canonicalized in Section 11.2) and issuing a new endorsement against that hash. This provides a mechanism for chained reputation without protocol-level signer endorsements.

12. Verification Procedure

A conforming verifier MUST perform the following steps in order for each signed section it intends to verify. Steps 12.1 through 12.6 are the cryptographic verification ("layer 1" in [HTMLTRUST-W3C]). Step 12.7 is the trust decision and is out of scope for this document; the verifier returns the cryptographic result to the trust-decision layer for further evaluation.

12.1. Step 1: Extract attributes and content

Obtain an accepted source representation under Section 4.1 and validate the portable parser profile before extracting the section. If source cannot be obtained, return "source-refetch-failed". If the source or parser diagnostics cannot establish the portable profile, return "parser-profile-unsupported". If a Section 12.10 resource ceiling is reached, return "resource-limit-exceeded". Stop after any of these failures.

Obtain the six required attributes profile, signature-scope, keyid, signature, content-hash, and algorithm, plus the inner content of the signed section. If any required attribute is missing or empty, or if a protocol attribute has leading or trailing ASCII whitespace, the verifier MUST return incomplete and stop.

Require profile to equal htmltrust-signature-v1; otherwise return profile-unsupported. Require signature-scope to equal url or origin; otherwise return scope-unsupported. Select the profile before performing canonicalization and do not try a different profile after a later failure.

Validate the content-hash and signature encodings against Section 6. A non-canonical Base64 value, an invalid hash shape, or a decoded hash with the wrong natural output length produces "invalid-encoding". If the signature or hash algorithm is not registered or is outside the verifier's accepted set, return "algorithm-not-supported". For Ed25519 and ECDSA, a decoded signature whose length or representation does not match Section 6.3 produces "malformed-signature". RSA signature length is checked after key resolution in Step 6.

12.2. Step 2: Canonicalize content; compute and compare content hash

Apply the canonicalization algorithm of Section 4 to the inner content. Compute the hash using the algorithm given by the prefix of the content-hash attribute. Compare the computed hash to the attribute value (excluding the prefix and separator, after Base64 decoding).

If URL resolution or another signed-attribute operation cannot produce the canonical value required by Section 4.3.2, the verifier MUST return attribute-canonicalization-failed and stop. If a resolved href or src violates htmltrust-safe-url-v1, return url-policy-violation and stop.

If the hashes do not match, the verifier MUST return a "content-hash-mismatch" failure and stop.

12.3. Step 3: Canonicalize claims; compute claims hash

Apply Section 4.6 to the claim <meta> element children of the signed section. Compute the hash using the same algorithm as Step 2. Form the claims-hash string per Section 6.2.

A missing name or content, or an empty normalized name, produces "claim-malformed". Duplicate normalized names produce "claim-duplicate".

Require exactly one normalized claim name equal to signed-at. If it is absent, return claim-missing. Validate its normalized content against Section 5.4; a failure produces timestamp-invalid.

12.4. Step 4: Construct signing payload

Derive location from the accepted source snapshot's final response URL and the signature-scope attribute under Section 5.3. If the document URL does not produce an allowed public v1 location, return origin-not-supported.

Construct the complete object in Section 5.5 from the validated attributes, the claims hash from Step 3, the derived location, and the normalized signed-at value. Insert the four profile constants from Section 5.1 and the fixed context string. Serialize the object with [RFC8785], then UTF-8 encode the result. Those bytes are the signing payload.

A section copied to a location outside its signed scope produces signature-invalid because the verifier constructs a different signed object. The verifier MUST NOT use author-controlled canonical-link metadata as the location.

12.5. Step 5: Resolve keyid

Resolve the keyid attribute per Section 8 to obtain a public key and the algorithm it is suitable for. A retrieval or DID-resolution failure produces "key-resolution-failed". A key response that violates the required document schema produces "malformed-key-document". A revoked key, or an expired key that Section 9 does not permit for this signature, produces "key-revoked". The verifier MUST stop after any of these results.

If the algorithm associated with the resolved key is incompatible with the algorithm attribute of the signed section, the verifier MUST return an "algorithm-mismatch" failure and stop.

12.6. Step 6: Verify signature

Verify the Base64-decoded signature against the signing payload from Step 4 using the public key from Step 5 and the algorithm given by the algorithm attribute. Before RSA verification, require the decoded signature to be exactly the resolved modulus width; otherwise return "malformed-signature". If the cryptographic operation completes and the signature does not verify, return "signature-invalid". The verifier MUST stop after either failure.

If the verification succeeds, the verifier MUST return a "valid" cryptographic outcome.

12.7. Step 7: Trust decision (out of scope)

The verifier returns the cryptographic outcome to the trust-decision layer (in browsers, the user agent's trust policy; see [HTMLTRUST-W3C]). The trust layer composes endorsement, reputation, and personal-trust-list inputs and MUST NOT alter the cryptographic outcome.

12.8. Failure outcomes

The following identifiers are the closed failure vocabulary for this revision. A verifier MAY attach private diagnostic detail, but the cryptographic result MUST use one of these identifiers and MUST never map a listed failure to valid:

incomplete, profile-unsupported, scope-unsupported, content-hash-mismatch, claim-missing, claim-malformed, claim-duplicate, timestamp-invalid, attribute-canonicalization-failed, parser-profile-unsupported, url-policy-violation, invalid-encoding, malformed-signature, signature-invalid, key-resolution-failed, malformed-key-document, key-revoked, algorithm-not-supported, algorithm-mismatch, origin-not-supported, resource-limit-exceeded, network-policy-blocked, source-refetch-failed, and directory-unavailable.

Failures that occur before a key is resolved MUST NOT trigger signature verification. Network and resource failures MUST NOT be retried without the same finite limits. User-agent integrations MUST expose at most the identifier and a privacy-safe explanation to page scripts.

directory-unavailable is reserved for an optional trust-directory consultation in the layer-2 processing model. A layer-1 cryptographic result MUST NOT use that identifier, and directory failure MUST NOT change a valid cryptographic result into an invalid one.

12.9. Verifier network fetch policy

This section defines a provisional network model for verifier-initiated fetches. It is expected to be refined as browser, extension, and native verifier implementations mature.

Remote key documents and trust-directory endpoints MUST be fetched over HTTPS URLs. Verifiers MUST NOT send cookies, HTTP authentication state, client certificates, or other ambient credentials on key or directory fetches by default. A browser verifier MAY send credentials only for a same-origin source refetch of the document being verified, and only when that refetch is needed to obtain the original server HTML for the [HTMLTRUST-W3C] processing model.

Verifier fetches SHOULD send no Referer header. If a platform cannot omit the header, it MUST trim it no less strictly than strict-origin-when-cross-origin.

Web-page JavaScript verifiers are subject to the Fetch CORS model and therefore require key and directory servers to opt in with appropriate CORS response headers. Browser extensions, native user agents, crawlers, and command-line verifiers MAY have broader network authority, but they SHOULD apply the same credential, referrer, redirect, timeout, and cache constraints for privacy and interoperability.

Verifiers MAY follow HTTPS-to-HTTPS redirects for key and directory fetches subject to a small implementation-defined limit. They MUST NOT follow redirects to non-HTTPS URLs. They SHOULD impose finite timeouts, SHOULD honor HTTP cache semantics [RFC9111], and SHOULD cap cached key freshness when no explicit freshness information is present.

Browser verifiers that fetch through the page's fetch context MUST account for service-worker interception. A service worker on the publisher origin MAY intercept a same-origin source refetch; service workers on unrelated origins MUST NOT be allowed to intercept key or directory fetches outside their normal scope. User agents that provide a privileged verification path SHOULD bypass page script and extension mutation when preserving the original response snapshot.

Trust-directory consultation MUST be user opt-in. If a directory fetch, key fetch, source refetch, CORS check, redirect policy, timeout, cache validation, or user-permission check fails, the verifier MUST surface a specific failure outcome such as "key-resolution-failed", "directory-unavailable", "source-refetch-failed", or "network-policy-blocked" rather than treating the content as valid.

12.10. Resource limits

Verifiers MUST apply finite resource limits before parsing or fetching untrusted input. The following limits are the interoperable baseline for this revision; an implementation MAY choose lower values, but MUST NOT silently continue beyond them:

Table 5
Resource Maximum
Source response or signed-section input 1 MiB
Canonical content output for one section 1 MiB
Direct-child claims in one section 64
Normalized claim name or value 4 KiB each
Remote key document, including headers 64 KiB
Endorsement response body 256 KiB
Endorsements processed for one content hash 100
HTTPS redirects for one verifier fetch 3
Wall-clock time for one verifier fetch 5 seconds
Concurrent verifier fetches per document 4

The limits apply independently to source verification, key resolution, and optional directory consultation. A verifier that reaches a limit MUST stop the affected operation, MUST NOT return a valid cryptographic result, and MUST report the machine-readable failure resource-limit-exceeded. It MUST NOT expose the rejected input or canonical bytes to page scripts. An implementation MAY expose the precise limit to privileged diagnostics.

13. Security Considerations

13.1. Threat model

This document considers the following adversaries.

Passive eavesdropper:

An adversary observing traffic on the network. Out of scope; this threat is addressed by TLS [RFC8446], on which HTMLTrust cryptographic verification does not directly depend.

Active rewriter:

An adversary capable of modifying HTML in transit or at a non- origin endpoint. HTMLTrust detects any modification of canonical text or signed semantic attributes inside a signed section. It does not detect modifications to surrounding markup or to non-signed regions; verifiers and trust policies MUST present cryptographic outcomes per signed section, not per document.

Origin compromise:

An adversary in control of the publisher origin. Such an adversary can serve content under an origin that the signer's binding permits. Detection requires the signer to publish content hashes to a trust directory or other independent index; the protocol does not prevent the attack in isolation.

Key compromise:

An adversary in possession of a signer's private key. HTMLTrust cannot prevent forgery in this case. Mitigations are revocation (Section 8.2, revoked field) and short-lived keys with expires.

Replay across origins:

An adversary copying a signed section from one site to another. Both v1 scopes bind an HTTPS tuple origin, so verification on another origin fails.

Replay within origin:

An adversary re-publishing signed content at another URL on the same origin. The recommended url scope prevents this by binding the serialized path and query. origin scope permits it as an explicit portability choice; verifiers surface that scope to trust and user-interface policy. A copied section can still be surrounded by hostile unsigned context at its original URL, so scope binding does not establish whole-page integrity.

Downgrade:

An adversary inducing a verifier to use a weak algorithm. Mitigated by signing the exact algorithm, key identifier, and profile identifiers, and by the verifier's right to refuse algorithms (Section 7.3). A verifier never retries under a legacy profile after a v1 failure.

13.2. Deliberate semantic-subset signing trade-off

This revision signs canonical text plus a small list of semantic attributes (href, src, alt, and aria-label). An adversary in possession of signed material MAY still rewrap it in misleading block-level markup, alter non-covered attributes, or surround it with hostile media without invalidating the cryptographic signature. The HTMLTrust system addresses these residual risks through layered means:

  • Location binding (Section 5) restricts the signed material to one URL by default, or to one origin after an explicit signer choice.

  • Trust-directory indexing and external research (out of band, but enabled by the content hash being globally addressable) catches altered surrounding context on mirror sites.

  • Future revisions MAY define a new signed-attribute profile with broader coverage, addressing a larger subset of context-swapping attacks.

The deliberate trade-off is implementational simplicity and cross-implementation stability versus full structural integrity. XML Digital Signatures [XMLDSIG] is the canonical cautionary tale for the latter end of that spectrum.

13.3. Claim metadata normalization and duplicate binding

All direct child claim <meta> elements participate in the claims hash. This prevents a metadata sanitizer from silently dropping, reordering, or renaming visible author claims without invalidating the signature, but it also means that sanitizers which remove unknown <meta> elements inside a signed section will break verification. Authoring tools SHOULD place only claims that they intend to sign as direct child <meta> elements and SHOULD keep unrelated machine metadata outside the signed section.

The signed-at value is intentionally included twice: once as a normal claim inside the claims hash and once as an explicit signing-payload field. The duplicate binding is not ambiguous because both uses consume the same normalized direct child claim. If the claim is missing, malformed, or duplicated after name normalization, verification fails before signature verification. This keeps timestamp extraction simple for directories and user interfaces while preserving the integrity of the complete claim set.

13.3.1. Injective record encoding

Claim names and values use the escaping rule in Section 4.6, and signed attribute values use the U+0040 escaping rule in Section 4.3.2. These rules make the record encodings injective: a literal text sequence cannot be mistaken for an attribute record, and a colon or reverse solidus in a claim cannot move the name/value boundary. The cases in vectors/claims-escaping.json are normative examples.

Implementations MUST apply the escaping before hashing; accepting the historical unescaped form is a legacy mode and MUST be clearly distinguished from this revision.

13.4. Revocation latency

Revocation under Section 8.2 depends on the verifier re-fetching the key document. Verifiers cache key documents subject to HTTP cache semantics [RFC9111]; an aggressive cache may serve stale key data after a revoked: true update. Verifiers SHOULD set a relatively short maximum cache age for HTMLTrust key documents (recommended: one hour) and SHOULD honor Cache-Control: no-cache directives from the key-document server.

13.5. Algorithm agility risks

The registry in Section 7 is designed to permit new algorithms without breaking deployed verifiers. However, a verifier that accepts the union of all registered algorithms inherits the security of the weakest. Verifiers SHOULD limit their accepted-algorithm set to those required by their threat model and SHOULD log or surface verifications performed under deprecated algorithms.

13.6. Side channels

A verifier MUST NOT distinguish verification failure from verification success via observable side channels (timing, network-request differences, console output visible to page scripts). The user-agent processing model in [HTMLTRUST-W3C] specifies the channel through which verification outcomes are exposed; verifiers in non-browser contexts SHOULD follow analogous discipline.

14. Privacy Considerations

14.1. Directory query exposure

Trust-directory queries during endorsement retrieval or reputation lookup reveal the requesting verifier's IP address, request headers, and the content hashes it is interested in. A directory operator that logs queries can derive a fingerprint of the verifier's reading patterns.

Mitigations:

  • Verifiers SHOULD cache directory responses (subject to [RFC9111]) to reduce repeated identical queries.

  • Verifiers MAY batch endorsement requests for multiple content hashes encountered in the same document.

  • Verifiers SHOULD support a "no directory" mode that limits the verifier to local cryptographic verification only.

  • Verifiers MAY use HTTP via Tor or another anonymizing transport for directory queries; directories SHOULD accept anonymized traffic.

14.2. Key resolution exposure

Resolving a keyid via the methods of Section 8 reveals the verifier's interest to the key-hosting server. For directory-hosted keys, this collapses into Section 14.1. For DID and direct-URL methods, the exposure is to the signer's chosen identity host.

Signers SHOULD NOT publish key documents from origins that would draw inferences from key-fetch traffic that would otherwise be unavailable.

14.3. Endorsement disclosure

An endorsement is intentionally public. Endorsers who do not wish their endorsement to be public-facing MUST NOT submit it to a public directory; the protocol does not provide a private- endorsement mechanism.

15. IANA Considerations

This document requests the following IANA actions.

15.1. Media types

The following media types are to be registered in the "Media Types" registry.

15.1.1. application/htmltrust-key+json

  • Type name: application

  • Subtype name: htmltrust-key+json

  • Required parameters: none

  • Optional parameters: charset (must be utf-8 if present)

  • Encoding considerations: binary

  • Security considerations: see Section 13 of this document

  • Interoperability considerations: see Section 8.2

  • Published specification: this document (Section 8.2)

  • Applications that use this media type: HTMLTrust verifiers, publishers, and trust directories

  • Fragment identifier considerations: none

  • Author / change controller: IETF / IESG

15.1.2. application/htmltrust-endorsement+json

Identical fields as Section 15.1.1, with subtype name htmltrust-endorsement+json and a reference to Section 11.

15.1.3. application/htmltrust-directory+json

Identical fields as Section 15.1.1, with subtype name htmltrust-directory+json and a reference to Section 10.2.

15.1.4. application/htmltrust-content+json

Identical fields as Section 15.1.1, with subtype name htmltrust-content+json and a reference to Section 10.3 (the /content/{hash} response format).

15.2. Well-known URI

The suffix htmltrust is to be registered in the "Well-Known URIs" registry per [RFC8615].

  • URI suffix: htmltrust

  • Change controller: IETF

  • Specification document: this document, Section 10.2

  • Related information: identifies trust directory discovery metadata

15.3. Signature algorithm registry

IANA is requested to establish a new registry titled "HTMLTrust Signature Algorithms" under a new "HTMLTrust Parameters" group. The registry is policy "Specification Required" per [RFC8126]; the initial contents are those of Section 7.1.

Each registration MUST include:

  • Identifier (the string used in the algorithm attribute)

  • Algorithm description

  • Reference

  • Status: "current" or "deprecated"

15.4. Hash algorithm registry

IANA is requested to establish a new registry titled "HTMLTrust Hash Algorithms" under the "HTMLTrust Parameters" group. The registry is policy "Specification Required". The initial contents are those of Section 7.2.

Each registration MUST include:

  • Identifier (the string used as the hash prefix in content-hash values)

  • Algorithm description

  • Output length in bytes

  • Reference

  • Status: "current" or "deprecated"

15.5. HTMLTrust error type prefix

This document does not request an IANA registry for the https://htmltrust.org/errors/... URIs used in problem-details responses (Section 10.9); those URIs are stewarded by the document author and the Community Group.

16. References

16.1. Normative References

[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC3339]
Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, , <https://www.rfc-editor.org/rfc/rfc3339>.
[RFC4648]
Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/rfc/rfc4648>.
[RFC5280]
Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., and W. Polk, "Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, , <https://www.rfc-editor.org/rfc/rfc5280>.
[RFC6454]
Barth, A., "The Web Origin Concept", RFC 6454, DOI 10.17487/RFC6454, , <https://www.rfc-editor.org/rfc/rfc6454>.
[RFC7515]
Jones, M., Bradley, J., and N. Sakimura, "JSON Web Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, , <https://www.rfc-editor.org/rfc/rfc7515>.
[RFC7517]
Jones, M., "JSON Web Key (JWK)", RFC 7517, DOI 10.17487/RFC7517, , <https://www.rfc-editor.org/rfc/rfc7517>.
[RFC7518]
Jones, M., "JSON Web Algorithms (JWA)", RFC 7518, DOI 10.17487/RFC7518, , <https://www.rfc-editor.org/rfc/rfc7518>.
[RFC8017]
Moriarty, K., Ed., Kaliski, B., Jonsson, J., and A. Rusch, "PKCS #1: RSA Cryptography Specifications Version 2.2", RFC 8017, DOI 10.17487/RFC8017, , <https://www.rfc-editor.org/rfc/rfc8017>.
[RFC8032]
Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital Signature Algorithm (EdDSA)", RFC 8032, DOI 10.17487/RFC8032, , <https://www.rfc-editor.org/rfc/rfc8032>.
[RFC8126]
Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, , <https://www.rfc-editor.org/rfc/rfc8126>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.
[RFC8615]
Nottingham, M., "Well-Known Uniform Resource Identifiers (URIs)", RFC 8615, DOI 10.17487/RFC8615, , <https://www.rfc-editor.org/rfc/rfc8615>.
[RFC8785]
Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, DOI 10.17487/RFC8785, , <https://www.rfc-editor.org/rfc/rfc8785>.
[RFC9110]
Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, , <https://www.rfc-editor.org/rfc/rfc9110>.
[RFC9111]
Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Caching", STD 98, RFC 9111, DOI 10.17487/RFC9111, , <https://www.rfc-editor.org/rfc/rfc9111>.
[RFC9421]
Backman, A., Ed., Richer, J., Ed., and M. Sporny, "HTTP Message Signatures", RFC 9421, DOI 10.17487/RFC9421, , <https://www.rfc-editor.org/rfc/rfc9421>.
[RFC9457]
Nottingham, M., Wilde, E., and S. Dalal, "Problem Details for HTTP APIs", RFC 9457, DOI 10.17487/RFC9457, , <https://www.rfc-editor.org/rfc/rfc9457>.
[RFC9530]
Polli, R. and L. Pardue, "Digest Fields", RFC 9530, DOI 10.17487/RFC9530, , <https://www.rfc-editor.org/rfc/rfc9530>.
[UNICODE-NFC]
The Unicode Consortium, "Unicode Standard Annex #15: Unicode Normalization Forms", , <https://www.unicode.org/reports/tr15/>.
[W3C.did-core]
W3C, "Decentralized Identifiers (DIDs) v1.0", , <https://www.w3.org/TR/did-core/>.

16.2. Informative References

[AI-CONTENT-DISCLOSURE]
W3C AI Content Disclosure Community Group, "AI Content Disclosure Community Group", , <https://www.w3.org/community/ai-content-disclosure/>.
[ANTIFRAUD-CG]
W3C Anti-Fraud Community Group, "Anti-Fraud Community Group", , <https://www.w3.org/community/antifraud/>.
[C2PA]
Coalition for Content Provenance and Authenticity, "C2PA Technical Specification 2.0", , <https://c2pa.org/specifications/specifications/2.0/specs/C2PA_Specification.html>.
[CREDENTIALS-CG]
W3C Credentials Community Group, "Credentials Community Group", , <https://www.w3.org/community/credentials/>.
[CREDIBLE-WEB]
W3C Credible Web Community Group, "Credible Web Community Group", , <https://credweb.org/>.
[DEFACTO]
W3C DeFacto Community Group, "Decentralized Fact-checking and Provenance Organization (DeFacto) Community Group", , <https://www.w3.org/community/defacto/>.
[ECOJI]
Turner, K., "Ecoji: A base-1024 encoding using emoji", , <https://github.com/keith-turner/ecoji>.
[HTMLTRUST-W3C]
Grey, J., "HTMLTrust: Signed Sections for the Web (W3C Community Group Report)", , <https://htmltrust.org/spec/>.
[ORIGINATOR-PROFILE]
"Originator Profile", , <https://originator-profile.org/>.
[RFC3161]
Adams, C., Cain, P., Pinkas, D., and R. Zuccherato, "Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP)", RFC 3161, DOI 10.17487/RFC3161, , <https://www.rfc-editor.org/rfc/rfc3161>.
[RFC6376]
Crocker, D., Ed., Hansen, T., Ed., and M. Kucherawy, Ed., "DomainKeys Identified Mail (DKIM) Signatures", STD 76, RFC 6376, DOI 10.17487/RFC6376, , <https://www.rfc-editor.org/rfc/rfc6376>.
[RFC8446]
Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, , <https://www.rfc-editor.org/rfc/rfc8446>.
[SRI]
W3C, "Subresource Integrity", , <https://www.w3.org/TR/SRI/>.
[SXG]
Yasskin, J., "Signed HTTP Exchanges", , <https://datatracker.ietf.org/doc/draft-yasskin-http-origin-signed-responses/>.
[XMLDSIG]
W3C, "XML Signature Syntax and Processing Version 2.0", , <https://www.w3.org/TR/xmldsig-core2/>.

Appendix A. Acknowledgements

The author thanks early reviewers of the HTMLTrust whitepaper for feedback that shaped this draft, and acknowledges the conceptual debt to DKIM [RFC6376], JSON Web Signature [RFC7515], JSON Canonicalization Scheme [RFC8785], HTTP Message Signatures [RFC9421], and Signed HTTP Exchanges [SXG]. The two-layer split of cryptographic verification from trust evaluation is influenced by the operational experience of DKIM deployment.

Appendix B. Test Vectors

This appendix gives a complete, reproducible signing-profile vector. Its cryptographic input starts with the canonical content and canonical claims byte strings shown below; parser and canonicalization behavior is covered by the separate Section 4 vectors. The same vector is maintained machine-readable as ietf-draft/vectors/signing-profile-v1.json in this repository. The vector checker hashes both canonical byte strings, reconstructs the signing object from fixed v1 constants, serializes it with [RFC8785], derives the fixed test key, and verifies the signature. All hashes are SHA-256 encoded as unpadded standard Base64 with the sha256: prefix (Section 6); the signature is Ed25519 over the Section 5 JCS payload, also unpadded standard Base64.

B.1. Test key (Ed25519)

The 32-byte private seed is the ASCII string htmltrust-test-vector-ed25519-01:

seed (hex):       68746d6c74727573742d746573742d766563746f722d656432353531392d3031
public key (hex): ae8e474e7921dd51be650dcf6847ab452dee63421339efc3d6b041b2e85f4c19

Public key (SubjectPublicKeyInfo, PEM):

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAro5HTnkh3VG+ZQ3PaEerRS3uY0ITOe/D1rBBsuhfTBk=
-----END PUBLIC KEY-----

B.2. Inputs

Final signed document URL: HTTPS://EXAMPLE.COM:443/essays/engines#analysis. The url scope excludes the fragment and produces the signed location https://example.com/essays/engines. The source snapshot's document base URL for resolving relative signed URL attributes is the same URL without the fragment. signed-at is 2026-01-15T12:00:00Z.

Signed section (source HTML):

<signed-section profile="htmltrust-signature-v1"
                signature-scope="url"
                keyid="https://keys.example/alice-2026.json"
                algorithm="ed25519"
                content-hash="sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8"
                signature="m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA">
<meta name="author" content="Ada Lovelace">
<meta name="signed-at" content="2026-01-15T12:00:00Z">
<meta name="claim:License" content="CC-BY-4.0">
<h1>On Analytical Engines</h1>
<p>The engine weaves algebraic patterns &mdash; just as the loom weaves flowers.</p>
<p>See <a href="/notes/engine">the notes</a> and <img src="/img/ada.png" alt="Portrait of Ada">.</p>
</signed-section>

B.3. Canonical content

The three claim <meta> elements are excluded from content (they contribute to the claims). &mdash; decodes and normalizes to -; the relative href/src values resolve against the base URL and serialize with the Web URL serializer. Canonical content (UTF-8, U+000A shown as literal line breaks):

On Analytical Engines
The engine weaves algebraic patterns - just as the loom weaves flowers.
See @attr:a:href:https://example.com/notes/engine
the notes and @attr:img:src:https://example.com/img/ada.png
@attr:img:alt:Portrait of Ada
.

Content hash:

sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8

B.4. Canonical claims

Sorted lexically by the UTF-8 bytes of the normalized name:

author:Ada Lovelace
claim\:License:CC-BY-4.0
signed-at:2026-01-15T12\:00\:00Z

(The serialized string ends with a trailing U+000A after the last record.) Claims hash:

sha256:Fk5udwCnu1au8v5oaBsU+aSB5S2zSLqoF0xXO6HrIn4

B.5. Signing payload and signature

Signing object before JCS serialization:

{
  "algorithm": "ed25519",
  "attributeProfile": "htmltrust-attrs-v1",
  "canonicalizationProfile": "htmltrust-c14n-v1",
  "claimsHash": "sha256:Fk5udwCnu1au8v5oaBsU+aSB5S2zSLqoF0xXO6HrIn4",
  "contentHash": "sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8",
  "context": "https://htmltrust.org/protocol/signed-section",
  "keyid": "https://keys.example/alice-2026.json",
  "location": "https://example.com/essays/engines",
  "profile": "htmltrust-signature-v1",
  "scope": "url",
  "signedAt": "2026-01-15T12:00:00Z",
  "urlProfile": "htmltrust-safe-url-v1"
}

JCS signing payload, shown on one line:

{"algorithm":"ed25519","attributeProfile":"htmltrust-attrs-v1","canonicalizationProfile":"htmltrust-c14n-v1","claimsHash":"sha256:Fk5udwCnu1au8v5oaBsU+aSB5S2zSLqoF0xXO6HrIn4","contentHash":"sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8","context":"https://htmltrust.org/protocol/signed-section","keyid":"https://keys.example/alice-2026.json","location":"https://example.com/essays/engines","profile":"htmltrust-signature-v1","scope":"url","signedAt":"2026-01-15T12:00:00Z","urlProfile":"htmltrust-safe-url-v1"}

Ed25519 signature over that payload, unpadded standard Base64:

m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA

Appendix C. Example Directory Exchange

C.1. Submitting a content record

Request:

POST /content HTTP/1.1
Host: directory.example
Date: Thu, 15 Jan 2026 12:00:00 GMT
Content-Type: application/json
Signature-Input: sig1=("@method" "@target-uri" "host" "date" "content-digest");created=1768478400;keyid="https://keys.example/alice-2026.json";alg="ed25519"
Signature: sig1=:gdWl4N5vUFjVBFg8HDJu49u03bCfSvu/m0A5Ql8omqsoBIbiMXiyyaRQmgBQ7pS7ze7dA0VSx1rD+VbBi2qADg:
Content-Digest: sha-256=:vVnocWvEKyG/MTZYW9GmhC8a+6EE3faenFcdA9UF8Ug:

{
  "profile": "htmltrust-signature-v1",
  "contentHash": "sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8",
  "keyid": "https://keys.example/alice-2026.json",
  "algorithm": "ed25519",
  "signedAt": "2026-01-15T12:00:00Z",
  "scope": "url",
  "location": "https://example.com/essays/engines",
  "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA",
  "sourceURL": "https://example.com/essays/engines",
  "claims": [{"name":"author","content":"Ada Lovelace"},{"name":"claim:License","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}]
}

Response:

HTTP/1.1 201 Created
Location: https://directory.example/content/sha256%3AIVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8
Content-Type: application/htmltrust-content+json

{
  "contentHash": "sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8",
  "firstSeen": "2026-05-15T12:34:56Z",
  "signers": [
    {
      "profile": "htmltrust-signature-v1",
      "keyid": "https://keys.example/alice-2026.json",
      "algorithm": "ed25519",
      "signedAt": "2026-01-15T12:00:00Z",
      "scope": "url",
      "location": "https://example.com/essays/engines",
      "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA"
    }
  ],
  "endorsementCount": 0
}

C.2. Submitting an endorsement

Request body (after canonicalization per Section 11.2 and signing):

{
  "endorser": "did:web:reviewer.example",
  "endorsement": "sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8",
  "algorithm": "ed25519",
  "timestamp": "2026-05-10T09:00:00Z",
  "claim": "Verified original publication.",
  "expires": "2026-12-31T00:00:00Z",
  "signature": "wRGf14sbPTYebexyPALRMCo122Gaei+wQaQIh/taes/LQWKTQTSW2yBDIOUkBkG/uDGMMU7D1W6AveDClZNPCg"
}

For the endorsement example, the JCS input (the document above with the signature member removed) is:

{"algorithm":"ed25519","claim":"Verified original publication.","endorsement":"sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8","endorser":"did:web:reviewer.example","expires":"2026-12-31T00:00:00Z","timestamp":"2026-05-10T09:00:00Z"}

The Ed25519 signature above is reproducible with the test seed in this appendix. Its canonical payload hash is sha256:sPBnoFyxiTmeBOQHaTyhdJy6prv3jmTdoTZpDXP5xrg.

Appendix D. Open Issues

The following issues are open in this revision and are expected to be addressed in subsequent revisions or in the companion W3C Community Group Report.

  1. Runtime DOM mutation. A data-htmltrust-ignore opt-out marker is reserved but not yet normative. See Section 4.7.

  2. Mandatory-to-implement key resolution methods. This revision requires verifiers to implement at least one of three methods. A future revision may strengthen the requirement, particularly around DID methods.

  3. Reputation-score interoperability. The /signers/{id}/reputation endpoint is intentionally directory-specific. A future revision may define a small interoperable subset.

Author's Address

Jason Grey
Improbability Engineers, LLC