diff --git a/README.md b/README.md index 5376e6c..0a2addb 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ make w3c # serve the W3C draft at http://localhost:8000 make w3c-check # render the W3C draft and fail on ReSpec diagnostics ``` -`make check` needs Bash, Git, grep, jq, and Node.js 22 or newer. The other +`make check` needs Bash, Git, grep, and Node.js 22 or newer. The other targets list their tool requirements below. ## Choose the document you need @@ -77,16 +77,17 @@ Then run `make paper`. The PDF is ignored by Git. The W3C draft uses ReSpec. Run `make w3c`, open , and inspect the rendered draft. `make w3c-check` uses pinned ReSpec 37.3.5 and writes its temporary snapshot under `$TMPDIR` when set, then `RUNNER_TEMP` or -`/var/tmp`. It needs Node.js 24 or newer and Chrome or Chromium. +`/var/tmp`. It needs Node.js 22 or newer and Chrome or Chromium. ## Understand implementation status The drafts define the target protocol. The reference repositories implement the stable `v0.2.2` canonicalization profile and the current end-to-end flow. -The newest draft rules for parser-profile rejection, U+0040 attribute-record -escaping, complete RFC 8785 processing, resource ceilings, and native browser -DOM integration still require downstream implementation work. The review -documents identify each remaining gap. +The newest draft rules still require downstream implementation work. These +include the v1 JCS signing object and location scope, safe signed URLs, +parser-profile rejection, U+0040 escaping, complete RFC 8785 processing, +resource ceilings, and the native browser lifecycle. The review documents +identify each remaining gap. Current prototype repositories: diff --git a/ietf-draft/README.md b/ietf-draft/README.md index d6e803f..cbc4c47 100644 --- a/ietf-draft/README.md +++ b/ietf-draft/README.md @@ -24,6 +24,8 @@ processing model lives in `../w3c-cg/`. - `vectors/attribute-records.json` — signed-attribute framing and escaping cases. - `vectors/nested-section.json` — nested signed-section boundary behavior. - `vectors/origin-serialization.json` — tuple-origin serialization and rejection cases. +- `vectors/signing-profile-v1.json`: the versioned signing object, URL scope, + timestamp, safe-URL, key, and Ed25519 signature vector. - `README.md` — this file. ## Before submitting diff --git a/ietf-draft/draft-grey-htmltrust-00.html b/ietf-draft/draft-grey-htmltrust-00.html index cca4ce5..5aec7c3 100644 --- a/ietf-draft/draft-grey-htmltrust-00.html +++ b/ietf-draft/draft-grey-htmltrust-00.html @@ -1400,10 +1400,25 @@

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 the publication origin and to a -resolvable signer identifier. Verifiers re-derive the canonical text, +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 @@ -1893,9 +1909,27 @@

Origin:
-

The Web origin at which signed content is canonically published, as -defined by [RFC6454]: the tuple of scheme, host, and port. HTMLTrust -serializes origins as described in Section 5.

+

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.

@@ -1919,9 +1953,9 @@

Publisher origin:

Serves the resulting HTML to verifiers over the Web. The publisher -origin is bound into the signing payload via the legacy-named domain -field (Section 5). The publisher origin and the signer MAY be different -entities; the origin attests transport, the signer attests authorship.

+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:
@@ -1957,8 +1991,8 @@

with the private key corresponding to the signer's key identifier.

  • -

    The signer emits the HTML region annotated with key identifier, -signature, content hash, and algorithm attributes per [HTMLTRUST-W3C].

    +

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

  • The publisher origin serves the resulting HTML.

    @@ -2007,7 +2041,7 @@

    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 -origin in the signing payload is derived from the signed document URL, not +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. @@ -2161,8 +2195,9 @@

    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.

    -

    The following signed semantic attributes also contribute to the -canonical content when present on an included element:

    +

    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:

    • href

      @@ -2177,9 +2212,9 @@

      aria-label

    -

    This first signed-attribute list is intentionally small and is open -for community feedback. Future revisions MAY add attributes to the -list, but verifiers for this revision MUST use exactly the list above.

    +

    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 @@ -2197,10 +2232,10 @@

    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 -parsing the attribute value as a URL against the signed document's base -URL and serializing the resulting URL using the Web URL serializer. If -URL parsing fails, verification MUST fail with -"attribute-canonicalization-failed". The normalized value MUST NOT +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 @@ -2213,11 +2248,9 @@

    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.

    -

    Editor's Note: The precise expansion list for signed semantic -attributes is expected to evolve. Candidate additions include title, -cite, image dimension attributes, and ARIA attributes used in name -computation, but this revision deliberately avoids full structural HTML -signing.

    +

    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.

    @@ -2453,12 +2486,16 @@

    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.

    -

    The signed-at claim is not special for purposes of the claims hash: +

    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.

    +without weakening claim integrity.

    @@ -2485,91 +2522,271 @@

    5. Signing Payload Binding

    -

    The signature is computed over the deterministic UTF-8 byte string -formed by joining four fields with a single ASCII colon (U+003A) -between each:

    -
    +

    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
    PurposeIdentifier
    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:

    +
    -content-hash ":" claims-hash ":" domain ":" signed-at
    -
    -
    -

    The fields are defined as follows.

    -
    -
    content-hash:
    -
    -

    The content hash as defined in Section 6.2, including the -algorithm prefix and separator (for example, -sha256:Zm9vYmFy...). Verifiers MUST use the literal value -serialized into the content-hash attribute of the signed section -element.

    +{ + "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.

    -
    -
    claims-hash:
    -
    -

    The claims hash, formatted identically to content-hash. The -claims hash is not carried in a separate attribute on the wire; it -is recomputed by the verifier from the canonical claims byte -string (Section 4.6) using the algorithm given by the prefix of -the content-hash attribute. The hash algorithm for both -content-hash and claims-hash MUST be the same.

    +
    +
    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.

    -
    -
    domain:
    -
    -

    A legacy field name retained for wire compatibility. Its value is -the serialized Web origin at which the content is canonically -published. The value MUST be the result of the URL Standard's origin -serialization algorithm for the document URL, with no path, query, -fragment, or credentials. Conforming public signatures MUST use an -HTTPS tuple origin. An HTTP tuple origin MAY be enabled by an explicitly -configured local-development profile, but that profile MUST be -distinguishable from normal verification and MUST NOT be accepted for -public content by default. Opaque origins (data:, file:, and -similar) are invalid. The URL Standard, rather than a host-language URL -serializer, defines lowercasing, IDNA A-label conversion, IPv6 bracket -syntax, and default port omission. For example, https://example.org -and https://example.org:8443 are distinct origins.

    +
    +
    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.

    -
    -
    signed-at:
    -
    -

    The value of the signed-at claim <meta> element, after the -normalization in Section 4.4 has been applied. The value MUST -conform to the date-time production of [RFC3339] with the time -offset given in UTC ("Z").

    +
    +
    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 fields MUST appear in the order shown. The separator MUST be a -single U+003A COLON. No leading or trailing whitespace is permitted.

    -
    -
    -

    -5.1. Identity not bound directly +

    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

    -

    The signer's identifier (keyid) is intentionally not included in -the signing payload. The identifier is implicit in key resolution -(Section 8): any attempt to claim a different identifier would -resolve to a different public key, and the signature would fail to -verify. Including the identifier explicitly would add a fragile -string-matching surface without strengthening the cryptographic -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.2. Why the publisher origin +
    +
    +

    +5.7. Legacy payloads

    -

    Binding the signature to the publication origin prevents verbatim -re-publication of signed content with the original signature -preserved. A reader who encounters apparently signed content at a -different origin will see a signature verification failure caused by -the origin mismatch alone, regardless of any other property of the -content. Legitimate re-publication is supported by a separate -wrapper-signing mechanism using a claim:CanonicalURL field (see -[HTMLTRUST-W3C]); the wrapper signer attests to a republication, not -to original authorship, and is responsible for the chain.

    +

    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.

    @@ -2706,8 +2923,8 @@

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

    - - +
    Table 1
    + @@ -2782,8 +2999,8 @@

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

    -
    Table 2
    Identifier
    - +
    Table 2
    + @@ -3138,17 +3355,14 @@

    "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, and -supportedAlgorithms are REQUIRED. Other fields are OPTIONAL.

    -

    JSON members named domain in this API carry the serialized origin -value defined for the signing payload in Section 5. The field name is -retained for compatibility with early tooling and MUST NOT be -interpreted as a host-only domain name.

    +

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

    @@ -3168,9 +3382,12 @@

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

     {
    +  "profile": "htmltrust-signature-v1",
       "contentHash": "sha256:Zm9vYmFy...",
    -  "keyid": "did:web:author.example",
    +  "keyid": "did:web:author.example#key-1",
    +  "algorithm": "ed25519",
       "signedAt": "2026-05-01T10:30:00Z",
    -  "domain": "https://author.example",
    +  "scope": "url",
    +  "location": "https://author.example/posts/123",
       "signature": "3q2+7w8NslfJ...",
       "sourceURL": "https://author.example/posts/123",
       "claims": [
    @@ -3210,7 +3430,12 @@ 

    [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 claims array MUST contain the complete set of direct-child claim +

    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, @@ -3220,10 +3445,10 @@

    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 +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.

    +return 4xx codes per Section 10.9.

    @@ -3383,8 +3608,8 @@

    11.1. Document shape

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

    -

    Table 3
    Identifier
    - +
    Table 3
    + @@ -3536,9 +3761,9 @@

    12. Verification Procedure

    A conforming verifier MUST perform the following steps in order for -each signed section it intends to verify. Steps 11.1 through 11.6 +each signed section it intends to verify. Steps 12.1 through 12.6 are the cryptographic verification ("layer 1" in [HTMLTRUST-W3C]). -Step 11.7 is the trust decision and is out of scope for this +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.

    @@ -3552,18 +3777,23 @@

    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 four required attributes keyid, signature, -content-hash, and algorithm and the inner content of the signed -section. If any required attribute is missing, the verifier MUST -return an "incomplete" outcome and stop.

    -

    Validate the content-hash and signature encodings against Section 6. +

    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.

    +resolution in Step 6.

    @@ -3578,7 +3808,8 @@

    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.

    +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.

    @@ -3593,6 +3824,9 @@

    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.

    @@ -3600,20 +3834,18 @@

    12.4. Step 4: Construct signing payload

    -

    Construct the byte string defined in Section 5 from content-hash -(verbatim from the attribute), the claims hash from Step 3, the -serialized origin of the current document (the legacy-named domain -field in Section 5), and the value -of the signed-at claim <meta> element after normalization -(Section 4.4). If the signed-at claim is absent, the verifier MUST -return a "claim-missing" failure and stop.

    -

    If the normalized signed-at value is not an RFC 3339 date-time with the -UTC Z offset required by Section 5, the verifier MUST return -"claim-malformed" and stop.

    -

    If the document URL does not produce an allowed tuple origin under Section 5, -the verifier MUST return "origin-not-supported" and stop. A signature copied -to another allowed origin fails cryptographic verification because the -verifier constructs the payload from the current document origin.

    +

    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.

    @@ -3669,9 +3901,12 @@

    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, content-hash-mismatch, +

    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, @@ -3740,8 +3975,8 @@

    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 4
    Field
    - +
    Table 4
    + @@ -3849,25 +4084,26 @@

    Replay across origins:
    -

    An adversary copying a signed section from one site to another. -Prevented by origin binding (Section 5); verification on the new -origin will fail.

    +

    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 (long-tail):
    +
    Replay within origin:
    -

    An adversary re-publishing old signed content on the same origin -out of context. Not prevented by the protocol; mitigated by -external research and by the inclusion of signed-at in the -signing payload, which dates the attestation.

    +

    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 the verifier's right to refuse algorithms (Section 7.3) and by -the requirement that algorithm mismatch produce a distinct -failure outcome.

    +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.

    @@ -3886,8 +4122,8 @@

    HTMLTrust system addresses these residual risks through layered means:

    • -

      Origin binding (Section 5) ensures that the signed material appears -only on the publication origin under the original signature.

      +

      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 @@ -3895,8 +4131,8 @@

      altered surrounding context on mirror sites.

    • -

      Future revisions MAY extend hash coverage to more semantic -attributes, addressing a larger subset of context-swapping attacks.

      +

      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 @@ -4417,14 +4653,16 @@

    Appendix B. Test Vectors

    -

    This appendix gives a complete, reproducible end-to-end vector. The same -vector is maintained machine-readable in the conformance suite at -conformance/vectors/vector-01.json in the canonicalization repository at -commit a756dbaf27fd32799da7f2a1276cc16964e8d41f -(https://github.com/HTMLTrust/htmltrust-canonicalization), where every -signer and verifier binding reproduces it in CI. All hashes are SHA-256 -encoded as unpadded standard Base64 with the sha256: prefix -(Section 6); the signature is Ed25519 over the Section 5 payload, also +

    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.

    @@ -4454,17 +4692,24 @@

    B.2. Inputs

    -

    Signed document base URL (for resolving relative signed URL attributes): -https://example.com/essays/engines. Serialized publisher origin -(domain field): https://example.com. signed-at: -2026-01-15T12:00:00Z.

    +

    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>
    +<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="license" content="CC-BY-4.0">
    +<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>
    @@ -4510,15 +4755,15 @@ 

     author:Ada Lovelace
    -license:CC-BY-4.0
    -signed-at:2026-01-15T12:00:00Z
    +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:LRDw8hP30g9AxXzw8wteFOZpDyD/hWUyP25Be8C0hBs
    +sha256:Fk5udwCnu1au8v5oaBsU+aSB5S2zSLqoF0xXO6HrIn4
     

    @@ -4528,18 +4773,37 @@

    B.5. Signing payload and signature

    -

    Signing payload (Section 5, four colon-joined fields):

    -
    +

    Signing object before JCS serialization:

    +
    -sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8:sha256:LRDw8hP30g9AxXzw8wteFOZpDyD/hWUyP25Be8C0hBs:https://example.com:2026-01-15T12:00:00Z
    +{
    +  "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:

    -
    +

    JCS signing payload, shown on one line:

    +
    -kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg
    +{"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
    +
    +
    @@ -4561,18 +4825,21 @@

    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="did:web:author.example";alg="ed25519" +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": "did:web:author.example", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg", + "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":"license","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] + "claims": [{"name":"author","content":"Ada Lovelace"},{"name":"claim:License","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] }

    @@ -4588,10 +4855,13 @@

    "firstSeen": "2026-05-15T12:34:56Z", "signers": [ { - "keyid": "did:web:author.example", + "profile": "htmltrust-signature-v1", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg" + "scope": "url", + "location": "https://example.com/essays/engines", + "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA" } ], "endorsementCount": 0 @@ -4643,26 +4913,19 @@

    Community Group Report.

    1. -

      Semantic attribute coverage. This revision signs canonicalized text -plus the provisional semantic attribute list href, src, alt, -and aria-label. The exact expansion list remains open for -community feedback. See Section 4.3.2 and the security -considerations.

      +

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

    2. -

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

      -
    3. -
    4. -

      Mandatory-to-implement key resolution methods. This revision +

      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.

      +around DID methods.

    5. -
    6. -

      Reputation-score interoperability. The /signers/{id}/reputation +

    7. +

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

      +may define a small interoperable subset.

    diff --git a/ietf-draft/draft-grey-htmltrust-00.md b/ietf-draft/draft-grey-htmltrust-00.md index 82c810e..ef58f9b 100644 --- a/ietf-draft/draft-grey-htmltrust-00.md +++ b/ietf-draft/draft-grey-htmltrust-00.md @@ -160,8 +160,9 @@ 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 the publication origin and to a -resolvable signer identifier. Verifiers re-derive the canonical text, +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. @@ -297,9 +298,21 @@ Endorsement: defined in Section 11. Origin: -: The Web origin at which signed content is canonically published, as - defined by [RFC6454]: the tuple of scheme, host, and port. HTMLTrust - serializes origins as described in Section 5. +: 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. # Architecture Overview @@ -313,9 +326,9 @@ Signer: Publisher origin: : Serves the resulting HTML to verifiers over the Web. The publisher - origin is bound into the signing payload via the legacy-named `domain` - field (Section 5). The publisher origin and the signer MAY be different - entities; the origin attests transport, the signer attests authorship. + 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 @@ -338,8 +351,8 @@ The basic data flow is: 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 key identifier, - signature, content hash, and algorithm attributes per [HTMLTRUST-W3C]. +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. @@ -376,7 +389,7 @@ 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 -origin in the signing payload is derived from the signed document URL, not +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 @@ -482,17 +495,18 @@ 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. -The following signed semantic attributes also contribute to the -canonical content when present on an included element: +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: - `href` - `src` - `alt` - `aria-label` -This first signed-attribute list is intentionally small and is open -for community feedback. Future revisions MAY add attributes to the -list, but verifiers for this revision MUST use exactly the list above. +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 @@ -512,10 +526,10 @@ U+000A, so consecutive attribute records need no additional separator. 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 -parsing the attribute value as a URL against the signed document's base -URL and serializing the resulting URL using the Web URL serializer. If -URL parsing fails, verification MUST fail with -"attribute-canonicalization-failed". The normalized value MUST NOT +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 @@ -530,11 +544,9 @@ 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. -Editor's Note: The precise expansion list for signed semantic -attributes is expected to evolve. Candidate additions include `title`, -`cite`, image dimension attributes, and ARIA attributes used in name -computation, but this revision deliberately avoids full structural HTML -signing. +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. ### Boundary-producing elements @@ -697,6 +709,11 @@ 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 ``. Section 5 also includes the normalized `signed-at` value as @@ -719,76 +736,209 @@ explicit experimental mode. # Signing Payload Binding -The signature is computed over the deterministic UTF-8 byte string -formed by joining four fields with a single ASCII colon (U+003A) -between each: +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. + +## Profile identifiers and wire attributes + +The identifiers fixed by this profile are: + +| 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. + +## 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. + +## 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 ``, 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`. + +## 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: ~~~ -content-hash ":" claims-hash ":" domain ":" signed-at +YYYY-MM-DDTHH:MM:SSZ ~~~ -The fields are defined as follows. - -content-hash: -: The content hash as defined in Section 6.2, including the - algorithm prefix and separator (for example, - `sha256:Zm9vYmFy...`). Verifiers MUST use the literal value - serialized into the `content-hash` attribute of the signed section - element. - -claims-hash: -: The claims hash, formatted identically to `content-hash`. The - claims hash is not carried in a separate attribute on the wire; it - is recomputed by the verifier from the canonical claims byte - string (Section 4.6) using the algorithm given by the prefix of - the `content-hash` attribute. The hash algorithm for both - `content-hash` and `claims-hash` MUST be the same. - -domain: -: A legacy field name retained for wire compatibility. Its value is - the serialized Web origin at which the content is canonically - published. The value MUST be the result of the URL Standard's origin - serialization algorithm for the document URL, with no path, query, - fragment, or credentials. Conforming public signatures MUST use an - HTTPS tuple origin. An HTTP tuple origin MAY be enabled by an explicitly - configured local-development profile, but that profile MUST be - distinguishable from normal verification and MUST NOT be accepted for - public content by default. Opaque origins (`data:`, `file:`, and - similar) are invalid. The URL Standard, rather than a host-language URL - serializer, defines lowercasing, IDNA A-label conversion, IPv6 bracket - syntax, and default port omission. For example, `https://example.org` - and `https://example.org:8443` are distinct origins. - -signed-at: -: The value of the `signed-at` claim `` element, after the - normalization in Section 4.4 has been applied. The value MUST - conform to the date-time production of [RFC3339] with the time - offset given in UTC ("Z"). - -The fields MUST appear in the order shown. The separator MUST be a -single U+003A COLON. No leading or trailing whitespace is permitted. - -## Identity not bound directly - -The signer's identifier (`keyid`) is intentionally not included in -the signing payload. The identifier is implicit in key resolution -(Section 8): any attempt to claim a different identifier would -resolve to a different public key, and the signature would fail to -verify. Including the identifier explicitly would add a fragile -string-matching surface without strengthening the cryptographic -binding. - -## Why the publisher origin - -Binding the signature to the publication origin prevents verbatim -re-publication of signed content with the original signature -preserved. A reader who encounters apparently signed content at a -different origin will see a signature verification failure caused by -the origin mismatch alone, regardless of any other property of the -content. Legitimate re-publication is supported by a separate -wrapper-signing mechanism using a `claim:CanonicalURL` field (see -[HTMLTRUST-W3C]); the wrapper signer attests to a republication, not -to original authorship, and is responsible for the chain. +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. + +## Canonical signing object + +The signer constructs this JSON object, with every member present: + +~~~ json +{ + "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. + +## 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. + +## 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. # Hash and Signature Encoding @@ -1164,18 +1314,14 @@ capabilities. The response media type is "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`, and -`supportedAlgorithms` are REQUIRED. Other fields are OPTIONAL. - -JSON members named `domain` in this API carry the serialized origin -value defined for the signing payload in Section 5. The field name is -retained for compatibility with early tooling and MUST NOT be -interpreted as a host-only domain name. +The fields `directory`, `version`, `capabilities`, `supportedAlgorithms`, and +`supportedProfiles` are REQUIRED. Other fields are OPTIONAL. ## GET /content/{hash} @@ -1192,9 +1338,12 @@ A successful response is JSON of the form: "firstSeen": "2026-05-15T12:34:56Z", "signers": [ { - "keyid": "did:web:author.example", + "profile": "htmltrust-signature-v1", + "keyid": "did:web:author.example#key-1", + "algorithm": "ed25519", "signedAt": "2026-05-01T10:30:00Z", - "domain": "https://author.example", + "scope": "url", + "location": "https://author.example/posts/123", "signature": "3q2+7w8NslfJ..." } ], @@ -1212,10 +1361,13 @@ request body is JSON of the form: ~~~ { + "profile": "htmltrust-signature-v1", "contentHash": "sha256:Zm9vYmFy...", - "keyid": "did:web:author.example", + "keyid": "did:web:author.example#key-1", + "algorithm": "ed25519", "signedAt": "2026-05-01T10:30:00Z", - "domain": "https://author.example", + "scope": "url", + "location": "https://author.example/posts/123", "signature": "3q2+7w8NslfJ...", "sourceURL": "https://author.example/posts/123", "claims": [ @@ -1231,6 +1383,12 @@ The submission MUST be authenticated by an HTTP Message Signature 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 @@ -1444,9 +1602,9 @@ endorsements. # Verification Procedure A conforming verifier MUST perform the following steps in order for -each signed section it intends to verify. Steps 11.1 through 11.6 +each signed section it intends to verify. Steps 12.1 through 12.6 are the cryptographic verification ("layer 1" in [HTMLTRUST-W3C]). -Step 11.7 is the trust decision and is out of scope for this +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. @@ -1459,10 +1617,16 @@ 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 four required attributes `keyid`, `signature`, -`content-hash`, and `algorithm` and the inner content of the signed -section. If any required attribute is missing, the verifier MUST -return an "incomplete" outcome and stop. +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 @@ -1483,7 +1647,8 @@ 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. +`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. @@ -1497,24 +1662,26 @@ 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`. + ## Step 4: Construct signing payload -Construct the byte string defined in Section 5 from `content-hash` -(verbatim from the attribute), the claims hash from Step 3, the -serialized origin of the current document (the legacy-named `domain` -field in Section 5), and the value -of the `signed-at` claim `` element after normalization -(Section 4.4). If the `signed-at` claim is absent, the verifier MUST -return a "claim-missing" failure and stop. +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`. -If the normalized `signed-at` value is not an RFC 3339 date-time with the -UTC `Z` offset required by Section 5, the verifier MUST return -"claim-malformed" and stop. +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. -If the document URL does not produce an allowed tuple origin under Section 5, -the verifier MUST return "origin-not-supported" and stop. A signature copied -to another allowed origin fails cryptographic verification because the -verifier constructs the payload from the current document origin. +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. ## Step 5: Resolve keyid @@ -1557,9 +1724,12 @@ 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`, `content-hash-mismatch`, +`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`, @@ -1682,21 +1852,22 @@ Key compromise: `expires`. Replay across origins: -: An adversary copying a signed section from one site to another. - Prevented by origin binding (Section 5); verification on the new - origin will fail. +: 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 (long-tail): -: An adversary re-publishing old signed content on the same origin - out of context. Not prevented by the protocol; mitigated by - external research and by the inclusion of `signed-at` in the - signing payload, which dates the attestation. +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 the verifier's right to refuse algorithms (Section 7.3) and by - the requirement that `algorithm` mismatch produce a distinct - failure outcome. + 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. ## Deliberate semantic-subset signing trade-off @@ -1707,13 +1878,13 @@ 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: -- Origin binding (Section 5) ensures that the signed material appears - only on the publication origin under the original signature. +- 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 extend hash coverage to more semantic - attributes, addressing a larger subset of context-swapping attacks. +- 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 @@ -1923,14 +2094,16 @@ the operational experience of DKIM deployment. # Test Vectors -This appendix gives a complete, reproducible end-to-end vector. The same -vector is maintained machine-readable in the conformance suite at -`conformance/vectors/vector-01.json` in the canonicalization repository at -commit `a756dbaf27fd32799da7f2a1276cc16964e8d41f` -(https://github.com/HTMLTrust/htmltrust-canonicalization), where every -signer and verifier binding reproduces it in CI. All hashes are SHA-256 -encoded as unpadded standard Base64 with the `sha256:` prefix -(Section 6); the signature is Ed25519 over the Section 5 payload, also +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. ## Test key (Ed25519) @@ -1953,18 +2126,25 @@ MCowBQYDK2VwAyEAro5HTnkh3VG+ZQ3PaEerRS3uY0ITOe/D1rBBsuhfTBk= ## Inputs -Signed document base URL (for resolving relative signed URL attributes): -`https://example.com/essays/engines`. Serialized publisher origin -(`domain` field): `https://example.com`. `signed-at`: -`2026-01-15T12:00:00Z`. +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): ~~~ html - + - +

    On Analytical Engines

    The engine weaves algebraic patterns — just as the loom weaves flowers.

    See the notes and Portrait of Ada.

    @@ -2000,29 +2180,48 @@ Sorted lexically by the UTF-8 bytes of the normalized name: ~~~ author:Ada Lovelace -license:CC-BY-4.0 -signed-at:2026-01-15T12:00:00Z +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:LRDw8hP30g9AxXzw8wteFOZpDyD/hWUyP25Be8C0hBs +sha256:Fk5udwCnu1au8v5oaBsU+aSB5S2zSLqoF0xXO6HrIn4 ~~~ ## Signing payload and signature -Signing payload (Section 5, four colon-joined fields): +Signing object before JCS serialization: +~~~ json +{ + "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" +} ~~~ -sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8:sha256:LRDw8hP30g9AxXzw8wteFOZpDyD/hWUyP25Be8C0hBs:https://example.com:2026-01-15T12:00:00Z + +JCS signing payload, shown on one line: + +~~~ json +{"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: ~~~ -kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg +m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA ~~~ # Example Directory Exchange @@ -2036,18 +2235,21 @@ 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="did:web:author.example";alg="ed25519" +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": "did:web:author.example", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg", + "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":"license","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] + "claims": [{"name":"author","content":"Ada Lovelace"},{"name":"claim:License","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] } ~~~ @@ -2063,10 +2265,13 @@ Content-Type: application/htmltrust-content+json "firstSeen": "2026-05-15T12:34:56Z", "signers": [ { - "keyid": "did:web:author.example", + "profile": "htmltrust-signature-v1", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg" + "scope": "url", + "location": "https://example.com/essays/engines", + "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA" } ], "endorsementCount": 0 @@ -2106,17 +2311,12 @@ 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. Semantic attribute coverage. This revision signs canonicalized text - plus the provisional semantic attribute list `href`, `src`, `alt`, - and `aria-label`. The exact expansion list remains open for - community feedback. See Section 4.3.2 and the security - considerations. -2. Runtime DOM mutation. A `data-htmltrust-ignore` opt-out marker +1. Runtime DOM mutation. A `data-htmltrust-ignore` opt-out marker is reserved but not yet normative. See Section 4.7. -3. Mandatory-to-implement key resolution methods. This revision +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. -4. Reputation-score interoperability. The `/signers/{id}/reputation` +3. Reputation-score interoperability. The `/signers/{id}/reputation` endpoint is intentionally directory-specific. A future revision may define a small interoperable subset. diff --git a/ietf-draft/draft-grey-htmltrust-00.txt b/ietf-draft/draft-grey-htmltrust-00.txt index c073c8d..6544df1 100644 --- a/ietf-draft/draft-grey-htmltrust-00.txt +++ b/ietf-draft/draft-grey-htmltrust-00.txt @@ -86,26 +86,26 @@ Table of Contents 3. Architecture Overview . . . . . . . . . . . . . . . . . . . . 8 4. Canonicalization . . . . . . . . . . . . . . . . . . . . . . 9 4.1. Inputs . . . . . . . . . . . . . . . . . . . . . . . . . 9 - 4.1.1. Portable parser profile . . . . . . . . . . . . . . . 10 + 4.1.1. Portable parser profile . . . . . . . . . . . . . . . 11 4.2. Walk and text extraction . . . . . . . . . . . . . . . . 11 - 4.3. Element categories . . . . . . . . . . . . . . . . . . . 11 - 4.3.1. Excluded elements . . . . . . . . . . . . . . . . . . 11 - 4.3.2. Included elements . . . . . . . . . . . . . . . . . . 12 - 4.3.3. Boundary-producing elements . . . . . . . . . . . . . 13 + 4.3. Element categories . . . . . . . . . . . . . . . . . . . 12 + 4.3.1. Excluded elements . . . . . . . . . . . . . . . . . . 12 + 4.3.2. Included elements . . . . . . . . . . . . . . . . . . 13 + 4.3.3. Boundary-producing elements . . . . . . . . . . . . . 14 4.4. Text normalization . . . . . . . . . . . . . . . . . . . 14 - 4.4.1. Rationale . . . . . . . . . . . . . . . . . . . . . . 14 - 4.4.2. Stripped formatting characters . . . . . . . . . . . 14 - 4.4.3. Whitespace . . . . . . . . . . . . . . . . . . . . . 15 + 4.4.1. Rationale . . . . . . . . . . . . . . . . . . . . . . 15 + 4.4.2. Stripped formatting characters . . . . . . . . . . . 15 + 4.4.3. Whitespace . . . . . . . . . . . . . . . . . . . . . 16 4.4.4. Punctuation normalization . . . . . . . . . . . . . . 16 - 4.5. Block structure . . . . . . . . . . . . . . . . . . . . . 16 + 4.5. Block structure . . . . . . . . . . . . . . . . . . . . . 17 4.6. Canonical claims . . . . . . . . . . . . . . . . . . . . 17 4.7. Opt-out marker (Editor's Note) . . . . . . . . . . . . . 18 5. Signing Payload Binding . . . . . . . . . . . . . . . . . . . 18 - 5.1. Identity not bound directly . . . . . . . . . . . . . . . 19 - 5.2. Why the publisher origin . . . . . . . . . . . . . . . . 19 - 6. Hash and Signature Encoding . . . . . . . . . . . . . . . . . 19 - 6.1. Encoding choice . . . . . . . . . . . . . . . . . . . . . 20 - 6.2. Content hash and claims hash . . . . . . . . . . . . . . 20 + 5.1. Profile identifiers and wire attributes . . . . . . . . . 19 + 5.2. Signed-attribute and safe-URL profiles . . . . . . . . . 19 + 5.3. Location scope and same-origin replay . . . . . . . . . . 20 + 5.4. Timestamp profile . . . . . . . . . . . . . . . . . . . . 21 + 5.5. Canonical signing object . . . . . . . . . . . . . . . . 21 @@ -114,54 +114,54 @@ Grey Expires 28 February 2027 [Page 2] Internet-Draft HTMLTrust August 2026 - 6.3. Signature . . . . . . . . . . . . . . . . . . . . . . . . 21 - 7. Algorithm Registry . . . . . . . . . . . . . . . . . . . . . 21 - 7.1. Signature algorithms . . . . . . . . . . . . . . . . . . 21 - 7.2. Hash algorithms . . . . . . . . . . . . . . . . . . . . . 22 - 7.3. Algorithm agility . . . . . . . . . . . . . . . . . . . . 23 - 8. Key Resolution . . . . . . . . . . . . . . . . . . . . . . . 23 - 8.1. DID method resolution . . . . . . . . . . . . . . . . . . 23 - 8.2. Direct HTTPS URL . . . . . . . . . . . . . . . . . . . . 23 - 8.3. Trust directory reference . . . . . . . . . . . . . . . . 25 - 8.4. Acceptance policy . . . . . . . . . . . . . . . . . . . . 25 - 9. Key Lifecycle . . . . . . . . . . . . . . . . . . . . . . . . 25 - 9.1. Key states . . . . . . . . . . . . . . . . . . . . . . . 25 - 9.2. Rotation and supersession . . . . . . . . . . . . . . . . 26 - 9.3. Revocation, compromise, and signed-at freshness . . . . . 26 - 9.4. Key loss and recovery . . . . . . . . . . . . . . . . . . 27 - 10. Trust Directory HTTP API . . . . . . . . . . . . . . . . . . 27 - 10.1. Base URL and discovery . . . . . . . . . . . . . . . . . 28 - 10.2. GET /.well-known/htmltrust . . . . . . . . . . . . . . . 28 - 10.3. GET /content/{hash} . . . . . . . . . . . . . . . . . . 28 - 10.4. POST /content . . . . . . . . . . . . . . . . . . . . . 29 - 10.5. GET /content/{hash}/endorsements . . . . . . . . . . . . 30 - 10.6. POST /endorsements . . . . . . . . . . . . . . . . . . . 30 - 10.7. GET /keys/{id} . . . . . . . . . . . . . . . . . . . . . 30 - 10.8. GET /signers/{id}/reputation . . . . . . . . . . . . . . 31 - 10.9. Authentication . . . . . . . . . . . . . . . . . . . . . 31 - 10.10. Errors . . . . . . . . . . . . . . . . . . . . . . . . . 32 - 11. Endorsement Format . . . . . . . . . . . . . . . . . . . . . 32 - 11.1. Document shape . . . . . . . . . . . . . . . . . . . . . 32 - 11.2. Canonicalization for signing . . . . . . . . . . . . . . 34 - 11.3. Revocation and expiry . . . . . . . . . . . . . . . . . 34 - 11.4. Endorsement of an endorsement . . . . . . . . . . . . . 35 - 12. Verification Procedure . . . . . . . . . . . . . . . . . . . 35 - 12.1. Step 1: Extract attributes and content . . . . . . . . . 35 + 5.6. Algorithm and signer binding . . . . . . . . . . . . . . 22 + 5.7. Legacy payloads . . . . . . . . . . . . . . . . . . . . . 23 + 6. Hash and Signature Encoding . . . . . . . . . . . . . . . . . 23 + 6.1. Encoding choice . . . . . . . . . . . . . . . . . . . . . 23 + 6.2. Content hash and claims hash . . . . . . . . . . . . . . 24 + 6.3. Signature . . . . . . . . . . . . . . . . . . . . . . . . 24 + 7. Algorithm Registry . . . . . . . . . . . . . . . . . . . . . 25 + 7.1. Signature algorithms . . . . . . . . . . . . . . . . . . 25 + 7.2. Hash algorithms . . . . . . . . . . . . . . . . . . . . . 25 + 7.3. Algorithm agility . . . . . . . . . . . . . . . . . . . . 26 + 8. Key Resolution . . . . . . . . . . . . . . . . . . . . . . . 26 + 8.1. DID method resolution . . . . . . . . . . . . . . . . . . 26 + 8.2. Direct HTTPS URL . . . . . . . . . . . . . . . . . . . . 27 + 8.3. Trust directory reference . . . . . . . . . . . . . . . . 28 + 8.4. Acceptance policy . . . . . . . . . . . . . . . . . . . . 28 + 9. Key Lifecycle . . . . . . . . . . . . . . . . . . . . . . . . 28 + 9.1. Key states . . . . . . . . . . . . . . . . . . . . . . . 28 + 9.2. Rotation and supersession . . . . . . . . . . . . . . . . 29 + 9.3. Revocation, compromise, and signed-at freshness . . . . . 29 + 9.4. Key loss and recovery . . . . . . . . . . . . . . . . . . 30 + 10. Trust Directory HTTP API . . . . . . . . . . . . . . . . . . 30 + 10.1. Base URL and discovery . . . . . . . . . . . . . . . . . 31 + 10.2. GET /.well-known/htmltrust . . . . . . . . . . . . . . . 31 + 10.3. GET /content/{hash} . . . . . . . . . . . . . . . . . . 31 + 10.4. POST /content . . . . . . . . . . . . . . . . . . . . . 32 + 10.5. GET /content/{hash}/endorsements . . . . . . . . . . . . 33 + 10.6. POST /endorsements . . . . . . . . . . . . . . . . . . . 33 + 10.7. GET /keys/{id} . . . . . . . . . . . . . . . . . . . . . 34 + 10.8. GET /signers/{id}/reputation . . . . . . . . . . . . . . 34 + 10.9. Authentication . . . . . . . . . . . . . . . . . . . . . 34 + 10.10. Errors . . . . . . . . . . . . . . . . . . . . . . . . . 35 + 11. Endorsement Format . . . . . . . . . . . . . . . . . . . . . 35 + 11.1. Document shape . . . . . . . . . . . . . . . . . . . . . 36 + 11.2. Canonicalization for signing . . . . . . . . . . . . . . 37 + 11.3. Revocation and expiry . . . . . . . . . . . . . . . . . 37 + 11.4. Endorsement of an endorsement . . . . . . . . . . . . . 38 + 12. Verification Procedure . . . . . . . . . . . . . . . . . . . 38 + 12.1. Step 1: Extract attributes and content . . . . . . . . . 38 12.2. Step 2: Canonicalize content; compute and compare content - hash . . . . . . . . . . . . . . . . . . . . . . . . . 36 - 12.3. Step 3: Canonicalize claims; compute claims hash . . . . 36 - 12.4. Step 4: Construct signing payload . . . . . . . . . . . 36 - 12.5. Step 5: Resolve keyid . . . . . . . . . . . . . . . . . 37 - 12.6. Step 6: Verify signature . . . . . . . . . . . . . . . . 37 - 12.7. Step 7: Trust decision (out of scope) . . . . . . . . . 37 - 12.8. Failure outcomes . . . . . . . . . . . . . . . . . . . . 37 - 12.9. Verifier network fetch policy . . . . . . . . . . . . . 38 - 12.10. Resource limits . . . . . . . . . . . . . . . . . . . . 39 - 13. Security Considerations . . . . . . . . . . . . . . . . . . . 40 - 13.1. Threat model . . . . . . . . . . . . . . . . . . . . . . 40 - 13.2. Deliberate semantic-subset signing trade-off . . . . . . 41 - 13.3. Claim metadata normalization and duplicate binding . . . 42 - 13.3.1. Injective record encoding . . . . . . . . . . . . . 42 + hash . . . . . . . . . . . . . . . . . . . . . . . . . 39 + 12.3. Step 3: Canonicalize claims; compute claims hash . . . . 39 + 12.4. Step 4: Construct signing payload . . . . . . . . . . . 39 + 12.5. Step 5: Resolve keyid . . . . . . . . . . . . . . . . . 40 + 12.6. Step 6: Verify signature . . . . . . . . . . . . . . . . 40 + 12.7. Step 7: Trust decision (out of scope) . . . . . . . . . 40 + 12.8. Failure outcomes . . . . . . . . . . . . . . . . . . . . 41 + 12.9. Verifier network fetch policy . . . . . . . . . . . . . 41 + 12.10. Resource limits . . . . . . . . . . . . . . . . . . . . 42 @@ -170,38 +170,43 @@ Grey Expires 28 February 2027 [Page 3] Internet-Draft HTMLTrust August 2026 - 13.4. Revocation latency . . . . . . . . . . . . . . . . . . . 43 - 13.5. Algorithm agility risks . . . . . . . . . . . . . . . . 43 - 13.6. Side channels . . . . . . . . . . . . . . . . . . . . . 43 - 14. Privacy Considerations . . . . . . . . . . . . . . . . . . . 43 - 14.1. Directory query exposure . . . . . . . . . . . . . . . . 43 - 14.2. Key resolution exposure . . . . . . . . . . . . . . . . 44 - 14.3. Endorsement disclosure . . . . . . . . . . . . . . . . . 44 - 15. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 44 - 15.1. Media types . . . . . . . . . . . . . . . . . . . . . . 44 - 15.1.1. application/htmltrust-key+json . . . . . . . . . . . 44 - 15.1.2. application/htmltrust-endorsement+json . . . . . . . 45 - 15.1.3. application/htmltrust-directory+json . . . . . . . . 45 - 15.1.4. application/htmltrust-content+json . . . . . . . . . 45 - 15.2. Well-known URI . . . . . . . . . . . . . . . . . . . . . 45 - 15.3. Signature algorithm registry . . . . . . . . . . . . . . 45 - 15.4. Hash algorithm registry . . . . . . . . . . . . . . . . 46 - 15.5. HTMLTrust error type prefix . . . . . . . . . . . . . . 46 - 16. References . . . . . . . . . . . . . . . . . . . . . . . . . 46 - 16.1. Normative References . . . . . . . . . . . . . . . . . . 46 - 16.2. Informative References . . . . . . . . . . . . . . . . . 48 - Appendix A. Acknowledgements . . . . . . . . . . . . . . . . . . 50 - Appendix B. Test Vectors . . . . . . . . . . . . . . . . . . . . 50 - B.1. Test key (Ed25519) . . . . . . . . . . . . . . . . . . . 50 - B.2. Inputs . . . . . . . . . . . . . . . . . . . . . . . . . 51 - B.3. Canonical content . . . . . . . . . . . . . . . . . . . . 51 - B.4. Canonical claims . . . . . . . . . . . . . . . . . . . . 51 - B.5. Signing payload and signature . . . . . . . . . . . . . . 52 - Appendix C. Example Directory Exchange . . . . . . . . . . . . . 52 - C.1. Submitting a content record . . . . . . . . . . . . . . . 52 - C.2. Submitting an endorsement . . . . . . . . . . . . . . . . 53 - Appendix D. Open Issues . . . . . . . . . . . . . . . . . . . . 53 - Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 54 + 13. Security Considerations . . . . . . . . . . . . . . . . . . . 43 + 13.1. Threat model . . . . . . . . . . . . . . . . . . . . . . 43 + 13.2. Deliberate semantic-subset signing trade-off . . . . . . 44 + 13.3. Claim metadata normalization and duplicate binding . . . 45 + 13.3.1. Injective record encoding . . . . . . . . . . . . . 45 + 13.4. Revocation latency . . . . . . . . . . . . . . . . . . . 46 + 13.5. Algorithm agility risks . . . . . . . . . . . . . . . . 46 + 13.6. Side channels . . . . . . . . . . . . . . . . . . . . . 46 + 14. Privacy Considerations . . . . . . . . . . . . . . . . . . . 46 + 14.1. Directory query exposure . . . . . . . . . . . . . . . . 46 + 14.2. Key resolution exposure . . . . . . . . . . . . . . . . 47 + 14.3. Endorsement disclosure . . . . . . . . . . . . . . . . . 47 + 15. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 47 + 15.1. Media types . . . . . . . . . . . . . . . . . . . . . . 47 + 15.1.1. application/htmltrust-key+json . . . . . . . . . . . 47 + 15.1.2. application/htmltrust-endorsement+json . . . . . . . 48 + 15.1.3. application/htmltrust-directory+json . . . . . . . . 48 + 15.1.4. application/htmltrust-content+json . . . . . . . . . 48 + 15.2. Well-known URI . . . . . . . . . . . . . . . . . . . . . 48 + 15.3. Signature algorithm registry . . . . . . . . . . . . . . 48 + 15.4. Hash algorithm registry . . . . . . . . . . . . . . . . 49 + 15.5. HTMLTrust error type prefix . . . . . . . . . . . . . . 49 + 16. References . . . . . . . . . . . . . . . . . . . . . . . . . 49 + 16.1. Normative References . . . . . . . . . . . . . . . . . . 49 + 16.2. Informative References . . . . . . . . . . . . . . . . . 51 + Appendix A. Acknowledgements . . . . . . . . . . . . . . . . . . 53 + Appendix B. Test Vectors . . . . . . . . . . . . . . . . . . . . 53 + B.1. Test key (Ed25519) . . . . . . . . . . . . . . . . . . . 53 + B.2. Inputs . . . . . . . . . . . . . . . . . . . . . . . . . 54 + B.3. Canonical content . . . . . . . . . . . . . . . . . . . . 54 + B.4. Canonical claims . . . . . . . . . . . . . . . . . . . . 54 + B.5. Signing payload and signature . . . . . . . . . . . . . . 55 + Appendix C. Example Directory Exchange . . . . . . . . . . . . . 55 + C.1. Submitting a content record . . . . . . . . . . . . . . . 55 + C.2. Submitting an endorsement . . . . . . . . . . . . . . . . 56 + Appendix D. Open Issues . . . . . . . . . . . . . . . . . . . . 57 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 57 1. Introduction @@ -216,11 +221,6 @@ Internet-Draft HTMLTrust August 2026 - - - - - Grey Expires 28 February 2027 [Page 4] Internet-Draft HTMLTrust August 2026 @@ -228,11 +228,13 @@ Internet-Draft HTMLTrust August 2026 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 the - publication origin and to a resolvable signer identifier. 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. + 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, @@ -275,8 +277,6 @@ Internet-Draft HTMLTrust August 2026 - - Grey Expires 28 February 2027 [Page 5] Internet-Draft HTMLTrust August 2026 @@ -405,9 +405,22 @@ Internet-Draft HTMLTrust August 2026 an opinion about a specific content hash at a specific point in time. The format is defined in Section 11. - Origin: The Web origin at which signed content is canonically - published, as defined by [RFC6454]: the tuple of scheme, host, and - port. HTMLTrust serializes origins as described in Section 5. + 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 @@ -419,10 +432,10 @@ Internet-Draft HTMLTrust August 2026 static-site generator, authoring tool). Publisher origin: Serves the resulting HTML to verifiers over the - Web. The publisher origin is bound into the signing payload via - the legacy-named domain field (Section 5). The publisher origin - and the signer MAY be different entities; the origin attests - transport, the signer attests authorship. + 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 @@ -430,6 +443,13 @@ Internet-Draft HTMLTrust August 2026 local except for key resolution, which MAY require a network fetch. + + +Grey Expires 28 February 2027 [Page 8] + +Internet-Draft HTMLTrust August 2026 + + 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 @@ -442,14 +462,6 @@ Internet-Draft HTMLTrust August 2026 1. The signer canonicalizes a content region per Section 4 and computes the content hash. - - - -Grey Expires 28 February 2027 [Page 8] - -Internet-Draft HTMLTrust August 2026 - - 2. The signer collects in-band claim metadata, canonicalizes the claims per Section 4.6, and computes the claims hash. @@ -457,9 +469,8 @@ Internet-Draft HTMLTrust August 2026 it with the private key corresponding to the signer's key identifier. - 4. The signer emits the HTML region annotated with key identifier, - signature, content hash, and algorithm attributes per - [HTMLTRUST-W3C]. + 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. @@ -488,6 +499,13 @@ Internet-Draft HTMLTrust August 2026 mutation; a post-mutation DOM is a different verification input and MUST be reported as such by the W3C processing model. + + +Grey Expires 28 February 2027 [Page 9] + +Internet-Draft HTMLTrust August 2026 + + 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. @@ -498,17 +516,9 @@ Internet-Draft HTMLTrust August 2026 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 - - - -Grey Expires 28 February 2027 [Page 9] - -Internet-Draft HTMLTrust August 2026 - - verifier operating outside a browser MUST receive the signed document - URL and MUST apply the same HTML base-URL algorithm. The origin in - the signing payload is derived from the signed document URL, not from + 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 @@ -537,6 +547,21 @@ Internet-Draft HTMLTrust August 2026 table. The abbreviated "common entity" tables shipped by some early implementations are NOT conforming. + + + + + + + + + + +Grey Expires 28 February 2027 [Page 10] + +Internet-Draft HTMLTrust August 2026 + + 4.1.1. Portable parser profile To make independent implementations interoperable, this revision @@ -554,14 +579,6 @@ Internet-Draft HTMLTrust August 2026 outside this revision, but MUST NOT identify that output as conforming to this revision. - - - -Grey Expires 28 February 2027 [Page 10] - -Internet-Draft HTMLTrust August 2026 - - The profile cases in vectors/parser-profile.json cover duplicate attributes, malformed nesting, table insertion, foreign content, and ambiguous character references. A conforming verifier MUST produce @@ -591,6 +608,16 @@ Internet-Draft HTMLTrust August 2026 * If the node is a Comment, ProcessingInstruction, or DocumentType, it is ignored and contributes nothing. + + + + + +Grey Expires 28 February 2027 [Page 11] + +Internet-Draft HTMLTrust August 2026 + + 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 @@ -611,13 +638,6 @@ Internet-Draft HTMLTrust August 2026 * style - - -Grey Expires 28 February 2027 [Page 11] - -Internet-Draft HTMLTrust August 2026 - - * template * noscript @@ -641,6 +661,19 @@ Internet-Draft HTMLTrust August 2026 bytes; in particular a 's href is NOT a signed semantic attribute. + + + + + + + + +Grey Expires 28 February 2027 [Page 12] + +Internet-Draft HTMLTrust August 2026 + + 4.3.2. Included elements All elements not in Section 4.3.1 are included. Their start and end @@ -648,8 +681,9 @@ Internet-Draft HTMLTrust August 2026 block boundaries per Section 4.5 and their descendant text nodes contribute to the canonical content. - The following signed semantic attributes also contribute to the - canonical content when present on an included element: + 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: * href @@ -659,20 +693,10 @@ Internet-Draft HTMLTrust August 2026 * aria-label - This first signed-attribute list is intentionally small and is open - for community feedback. Future revisions MAY add attributes to the - list, but verifiers for this revision MUST use exactly the list - above. - - - - - - -Grey Expires 28 February 2027 [Page 12] - -Internet-Draft HTMLTrust August 2026 - + 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 @@ -690,31 +714,37 @@ Internet-Draft HTMLTrust August 2026 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 parsing the - attribute value as a URL against the signed document's base URL and - serializing the resulting URL using the Web URL serializer. If URL - parsing fails, verification MUST fail with "attribute- - canonicalization-failed". 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. + 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 + + + +Grey Expires 28 February 2027 [Page 13] + +Internet-Draft HTMLTrust August 2026 + + + 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. - Editor's Note: The precise expansion list for signed semantic - attributes is expected to evolve. Candidate additions include title, - cite, image dimension attributes, and ARIA attributes used in name - computation, but this revision deliberately avoids full structural - HTML signing. + 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 @@ -722,14 +752,6 @@ Internet-Draft HTMLTrust August 2026 content (see Section 4.5). The set of boundary-producing element names is exactly: - - - -Grey Expires 28 February 2027 [Page 13] - -Internet-Draft HTMLTrust August 2026 - - 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- @@ -750,6 +772,20 @@ Internet-Draft HTMLTrust August 2026 4. Apply the punctuation normalizations defined in Section 4.4.4. + + + + + + + + + +Grey Expires 28 February 2027 [Page 14] + +Internet-Draft HTMLTrust August 2026 + + 4.4.1. Rationale These normalizations make the canonical form stable under the silent @@ -778,14 +814,6 @@ Internet-Draft HTMLTrust August 2026 * U+034F COMBINING GRAPHEME JOINER - - - -Grey Expires 28 February 2027 [Page 14] - -Internet-Draft HTMLTrust August 2026 - - * U+061C ARABIC LETTER MARK * U+180E MONGOLIAN VOWEL SEPARATOR @@ -807,6 +835,13 @@ Internet-Draft HTMLTrust August 2026 * U+FFF9..U+FFFC INTERLINEAR ANNOTATION ANCHORS AND OBJECT REPLACEMENT + + +Grey Expires 28 February 2027 [Page 15] + +Internet-Draft HTMLTrust August 2026 + + The following characters MUST be preserved despite their visual invisibility. They are semantically significant in major writing systems. @@ -834,14 +869,6 @@ Internet-Draft HTMLTrust August 2026 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 - - - -Grey Expires 28 February 2027 [Page 15] - -Internet-Draft HTMLTrust August 2026 - - preservation inside
     (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-
    @@ -861,6 +888,16 @@ Internet-Draft                  HTMLTrust                    August 2026
        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.
     
    +
    +
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 16]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        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.
    @@ -885,19 +922,6 @@ Internet-Draft                  HTMLTrust                    August 2026
     
        The result is a UTF-8 byte string.
     
    -
    -
    -
    -
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 16]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
     4.6.  Canonical claims
     
        In-band claim metadata is carried by  elements that appear as
    @@ -923,6 +947,13 @@ Internet-Draft                  HTMLTrust                    August 2026
     
            escaped-name : escaped-content \n
     
    +
    +
    +Grey                    Expires 28 February 2027               [Page 17]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
            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.
    @@ -938,6 +969,11 @@ Internet-Draft                  HTMLTrust                    August 2026
        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 .  Section 5 also includes the normalized signed-at value
    @@ -945,15 +981,6 @@ Internet-Draft                  HTMLTrust                    August 2026
        binding makes the timestamp easy for verifiers and directory indexes
        to extract without weakening claim integrity.
     
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 17]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
     4.7.  Opt-out marker (Editor's Note)
     
        Editor's Note: Common runtime decorations (copy-to-clipboard buttons,
    @@ -969,103 +996,257 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     5.  Signing Payload Binding
     
    -   The signature is computed over the deterministic UTF-8 byte string
    -   formed by joining four fields with a single ASCII colon (U+003A)
    -   between each:
    +   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.
     
    -   content-hash ":" claims-hash ":" domain ":" signed-at
     
    -   The fields are defined as follows.
     
    -   content-hash:  The content hash as defined in Section 6.2, including
    -      the algorithm prefix and separator (for example,
    -      sha256:Zm9vYmFy...).  Verifiers MUST use the literal value
    -      serialized into the content-hash attribute of the signed section
    -      element.
    +Grey                    Expires 28 February 2027               [Page 18]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
     
    -   claims-hash:  The claims hash, formatted identically to content-hash.
    -      The claims hash is not carried in a separate attribute on the
    -      wire; it is recomputed by the verifier from the canonical claims
    -      byte string (Section 4.6) using the algorithm given by the prefix
    -      of the content-hash attribute.  The hash algorithm for both
    -      content-hash and claims-hash MUST be the same.
     
    -   domain:  A legacy field name retained for wire compatibility.  Its
    +5.1.  Profile identifiers and wire attributes
     
    +   The identifiers fixed by this profile are:
     
    +           +==========================+========================+
    +           | 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  |
    +           +--------------------------+------------------------+
     
    +                                  Table 1
     
    +   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
     
     
     
    +Grey                    Expires 28 February 2027               [Page 19]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
     
     
    -Grey                    Expires 28 February 2027               [Page 18]
    +   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 , 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.
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 20]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    -      value is the serialized Web origin at which the content is
    -      canonically published.  The value MUST be the result of the URL
    -      Standard's origin serialization algorithm for the document URL,
    -      with no path, query, fragment, or credentials.  Conforming public
    -      signatures MUST use an HTTPS tuple origin.  An HTTP tuple origin
    -      MAY be enabled by an explicitly configured local-development
    -      profile, but that profile MUST be distinguishable from normal
    -      verification and MUST NOT be accepted for public content by
    -      default.  Opaque origins (data:, file:, and similar) are invalid.
    -      The URL Standard, rather than a host-language URL serializer,
    -      defines lowercasing, IDNA A-label conversion, IPv6 bracket syntax,
    -      and default port omission.  For example, https://example.org and
    -      https://example.org:8443 are distinct origins.
    -
    -   signed-at:  The value of the signed-at claim  element, after
    -      the normalization in Section 4.4 has been applied.  The value MUST
    -      conform to the date-time production of [RFC3339] with the time
    -      offset given in UTC ("Z").
    -
    -   The fields MUST appear in the order shown.  The separator MUST be a
    -   single U+003A COLON.  No leading or trailing whitespace is permitted.
    -
    -5.1.  Identity not bound directly
    -
    -   The signer's identifier (keyid) is intentionally not included in the
    -   signing payload.  The identifier is implicit in key resolution
    -   (Section 8): any attempt to claim a different identifier would
    -   resolve to a different public key, and the signature would fail to
    -   verify.  Including the identifier explicitly would add a fragile
    -   string-matching surface without strengthening the cryptographic
    -   binding.
    -
    -5.2.  Why the publisher origin
    -
    -   Binding the signature to the publication origin prevents verbatim re-
    -   publication of signed content with the original signature preserved.
    -   A reader who encounters apparently signed content at a different
    -   origin will see a signature verification failure caused by the origin
    -   mismatch alone, regardless of any other property of the content.
    -   Legitimate re-publication is supported by a separate wrapper-signing
    -   mechanism using a claim:CanonicalURL field (see [HTMLTRUST-W3C]); the
    -   wrapper signer attests to a republication, not to original
    -   authorship, and is responsible for the chain.
    +5.4.  Timestamp profile
     
    -6.  Hash and Signature Encoding
    +   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:
     
    -Grey                    Expires 28 February 2027               [Page 19]
    +   {
    +     "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.
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 21]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   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.
    +
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 22]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
    +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
    @@ -1101,6 +1282,14 @@ Internet-Draft                  HTMLTrust                    August 2026
        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
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 23]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        [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.
    @@ -1115,13 +1304,6 @@ Internet-Draft                  HTMLTrust                    August 2026
     
        3.  The Base64-encoded, unpadded hash output.
     
    -
    -
    -Grey                    Expires 28 February 2027               [Page 20]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
        For example, a SHA-256 hash of an empty string is:
     
        sha256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU
    @@ -1155,6 +1337,15 @@ Internet-Draft                  HTMLTrust                    August 2026
           SHA-256, and a 32-octet salt. rsa-pkcs1-sha256 uses RSASSA-
           PKCS1-v1_5 with SHA-256.
     
    +
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 24]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        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
    @@ -1170,14 +1361,6 @@ Internet-Draft                  HTMLTrust                    August 2026
        The following identifiers are defined for use in the algorithm
        attribute of a signed section.
     
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 21]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
        +==================+===================================+===========+
        | Identifier       | Algorithm                         | Reference |
        +==================+===================================+===========+
    @@ -1193,7 +1376,7 @@ Internet-Draft                  HTMLTrust                    August 2026
        | rsa-pkcs1-sha256 | RSASSA-PKCS1-v1_5 with SHA-256    | [RFC8017] |
        +------------------+-----------------------------------+-----------+
     
    -                                 Table 1
    +                                 Table 2
     
        The mandatory-to-implement algorithm for both signers and verifiers
        is ed25519.  A verifier MAY accept additional algorithms; a verifier
    @@ -1212,6 +1395,13 @@ Internet-Draft                  HTMLTrust                    August 2026
        The following identifiers are defined for use in the algorithm prefix
        of content-hash and claims-hash.
     
    +
    +
    +Grey                    Expires 28 February 2027               [Page 25]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
                     +============+===========+===============+
                     | Identifier | Algorithm | Output length |
                     +============+===========+===============+
    @@ -1222,17 +1412,7 @@ Internet-Draft                  HTMLTrust                    August 2026
                     | sha512     | SHA-512   | 64 bytes      |
                     +------------+-----------+---------------+
     
    -                                 Table 2
    -
    -
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 22]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    +                                 Table 3
     
        The mandatory-to-implement hash algorithm is sha256.  The hash
        algorithm used for content-hash and claims-hash within a single
    @@ -1270,6 +1450,14 @@ Internet-Draft                  HTMLTrust                    August 2026
        of the DID document; if no such method exists, the verifier MAY use
        any verification method of compatible type.
     
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 26]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        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"
    @@ -1283,13 +1471,6 @@ Internet-Draft                  HTMLTrust                    August 2026
        application/jwk+json per [RFC7517]; verifiers MAY accept application/
        json for backward compatibility.
     
    -
    -
    -Grey                    Expires 28 February 2027               [Page 23]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
        The retrieved document MUST be one of:
     
        *  A JSON Web Key per [RFC7517], or
    @@ -1321,6 +1502,18 @@ Internet-Draft                  HTMLTrust                    August 2026
        resolution-failed" result.  The algorithm value and the decoded key
        type and parameters MUST match Section 7.1.
     
    +
    +
    +
    +
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 27]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        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;
    @@ -1335,17 +1528,6 @@ Internet-Draft                  HTMLTrust                    August 2026
        mismatch".  A conforming verifier MUST NOT use a PEM compatibility
        field or silently try a different key encoding.
     
    -
    -
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 24]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
     8.3.  Trust directory reference
     
        A keyid whose value is an absolute URL pointing at a trust
    @@ -1381,6 +1563,13 @@ Internet-Draft                  HTMLTrust                    August 2026
        A published key is in exactly one of four states from a verifier's
        perspective:
     
    +
    +
    +Grey                    Expires 28 February 2027               [Page 28]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        *  *active* -- neither expired nor revoked; usable for verification.
     
        *  *expired* -- the key document carries an expires timestamp in the
    @@ -1394,14 +1583,6 @@ Internet-Draft                  HTMLTrust                    August 2026
           signals suspected or confirmed key compromise.  See Revocation,
           compromise, and signed-at freshness, below.
     
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 25]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
        *  *superseded* -- the key is expired and the publisher has
           designated a successor (see Rotation and supersession, below).
           This is the normal rotation end-state.
    @@ -1436,28 +1617,23 @@ Internet-Draft                  HTMLTrust                    August 2026
        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.
    -
     
     
     
     
    -
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 26]
    +Grey                    Expires 28 February 2027               [Page 29]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   *  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
    @@ -1499,21 +1675,18 @@ Internet-Draft                  HTMLTrust                    August 2026
        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.
    -
     
     
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 27]
    +Grey                    Expires 28 February 2027               [Page 30]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   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
    @@ -1541,17 +1714,13 @@ Internet-Draft                  HTMLTrust                    August 2026
            "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, and supportedAlgorithms
    -   are REQUIRED.  Other fields are OPTIONAL.
    -
    -   JSON members named domain in this API carry the serialized origin
    -   value defined for the signing payload in Section 5.  The field name
    -   is retained for compatibility with early tooling and MUST NOT be
    -   interpreted as a host-only domain name.
    +   The fields directory, version, capabilities, supportedAlgorithms, and
    +   supportedProfiles are REQUIRED.  Other fields are OPTIONAL.
     
     10.3.  GET /content/{hash}
     
    @@ -1564,8 +1733,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -
    -Grey                    Expires 28 February 2027               [Page 28]
    +Grey                    Expires 28 February 2027               [Page 31]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -1575,9 +1743,12 @@ Internet-Draft                  HTMLTrust                    August 2026
          "firstSeen": "2026-05-15T12:34:56Z",
          "signers": [
            {
    -         "keyid": "did:web:author.example",
    +         "profile": "htmltrust-signature-v1",
    +         "keyid": "did:web:author.example#key-1",
    +         "algorithm": "ed25519",
              "signedAt": "2026-05-01T10:30:00Z",
    -         "domain": "https://author.example",
    +         "scope": "url",
    +         "location": "https://author.example/posts/123",
              "signature": "3q2+7w8NslfJ..."
            }
          ],
    @@ -1593,10 +1764,13 @@ Internet-Draft                  HTMLTrust                    August 2026
        request body is JSON of the form:
     
        {
    +     "profile": "htmltrust-signature-v1",
          "contentHash": "sha256:Zm9vYmFy...",
    -     "keyid": "did:web:author.example",
    +     "keyid": "did:web:author.example#key-1",
    +     "algorithm": "ed25519",
          "signedAt": "2026-05-01T10:30:00Z",
    -     "domain": "https://author.example",
    +     "scope": "url",
    +     "location": "https://author.example/posts/123",
          "signature": "3q2+7w8NslfJ...",
          "sourceURL": "https://author.example/posts/123",
          "claims": [
    @@ -1611,21 +1785,29 @@ Internet-Draft                  HTMLTrust                    August 2026
        re-verify the submitted signature against the canonical signing
        payload (Section 5) before indexing.
     
    -   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
     
     
     
    -Grey                    Expires 28 February 2027               [Page 29]
    +
    +Grey                    Expires 28 February 2027               [Page 32]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   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
    @@ -1661,26 +1843,26 @@ Internet-Draft                  HTMLTrust                    August 2026
        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.
    +Grey                    Expires 28 February 2027               [Page 33]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
     
    -   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.
     
    +   Successful submission returns 201 Created.  Endorsements with invalid
    +   signatures MUST be rejected with 400 Bad Request.
     
    +10.7.  GET /keys/{id}
     
    -Grey                    Expires 28 February 2027               [Page 30]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    +   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
     
    @@ -1716,6 +1898,14 @@ Internet-Draft                  HTMLTrust                    August 2026
        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
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 34]
    +
    +Internet-Draft                  HTMLTrust                    August 2026
    +
    +
        otherwise valid requests.
     
        The alg parameter identifies the HTTP Message Signature algorithm
    @@ -1727,17 +1917,6 @@ Internet-Draft                  HTMLTrust                    August 2026
        separately documented deployment convenience, but are not a
        conforming substitute for this authentication profile.
     
    -
    -
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 31]
    -
    -Internet-Draft                  HTMLTrust                    August 2026
    -
    -
        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
    @@ -1776,24 +1955,17 @@ Internet-Draft                  HTMLTrust                    August 2026
        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.
    -
    -
    -
    -
     
     
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 32]
    +Grey                    Expires 28 February 2027               [Page 35]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +11.1.  Document shape
    +
    +   The endorsement document is a JSON object with the following fields.
    +
        +=============+========+==========+=================================+
        | Field       | Type   | Required | Description                     |
        +=============+========+==========+=================================+
    @@ -1831,7 +2003,7 @@ Internet-Draft                  HTMLTrust                    August 2026
        |             |        |          | hash.                           |
        +-------------+--------+----------+---------------------------------+
     
    -                                  Table 3
    +                                  Table 4
     
        Additional fields MAY be present and SHOULD be preserved by
        directories.  Verifiers MUST ignore fields they do not recognize for
    @@ -1841,11 +2013,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 33]
    +Grey                    Expires 28 February 2027               [Page 36]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -1901,7 +2069,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 34]
    +Grey                    Expires 28 February 2027               [Page 37]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -1918,9 +2086,9 @@ Internet-Draft                  HTMLTrust                    August 2026
     12.  Verification Procedure
     
        A conforming verifier MUST perform the following steps in order for
    -   each signed section it intends to verify.  Steps 11.1 through 11.6
    +   each signed section it intends to verify.  Steps 12.1 through 12.6
        are the cryptographic verification ("layer 1" in [HTMLTRUST-W3C]).
    -   Step 11.7 is the trust decision and is out of scope for this
    +   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.
     
    @@ -1934,20 +2102,17 @@ Internet-Draft                  HTMLTrust                    August 2026
        ceiling is reached, return "resource-limit-exceeded".  Stop after any
        of these failures.
     
    -   Obtain the four required attributes keyid, signature, content-hash,
    -   and algorithm and the inner content of the signed section.  If any
    -   required attribute is missing, the verifier MUST return an
    -   "incomplete" outcome and stop.
    +   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.
     
    -   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.
    +   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.
     
     
     
    @@ -1957,11 +2122,24 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 35]
    +
    +
    +
    +Grey                    Expires 28 February 2027               [Page 38]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   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
    @@ -1972,7 +2150,9 @@ Internet-Draft                  HTMLTrust                    August 2026
     
        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.
    +   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.
    @@ -1987,37 +2167,37 @@ Internet-Draft                  HTMLTrust                    August 2026
        "claim-malformed".  Duplicate normalized names produce "claim-
        duplicate".
     
    -12.4.  Step 4: Construct signing payload
    -
    -   Construct the byte string defined in Section 5 from content-hash
    -   (verbatim from the attribute), the claims hash from Step 3, the
    -   serialized origin of the current document (the legacy-named domain
    -   field in Section 5), and the value of the signed-at claim 
    -   element after normalization (Section 4.4).  If the signed-at claim is
    -   absent, the verifier MUST return a "claim-missing" failure and stop.
    -
    -   If the normalized signed-at value is not an RFC 3339 date-time with
    -   the UTC Z offset required by Section 5, the verifier MUST return
    -   "claim-malformed" and stop.
    -
    -   If the document URL does not produce an allowed tuple origin under
    -   Section 5, the verifier MUST return "origin-not-supported" and stop.
    -   A signature copied to another allowed origin fails cryptographic
    -   verification because the verifier constructs the payload from the
    -   current document origin.
    -
    -
    +   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.
     
     
     
     
    -Grey                    Expires 28 February 2027               [Page 36]
    +Grey                    Expires 28 February 2027               [Page 39]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   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
    @@ -2053,34 +2233,30 @@ Internet-Draft                  HTMLTrust                    August 2026
        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:
    -
    -
    -
    -
    -
     
     
     
     
    -
    -Grey                    Expires 28 February 2027               [Page 37]
    +Grey                    Expires 28 February 2027               [Page 40]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    -   incomplete, content-hash-mismatch, claim-missing, claim-malformed,
    -   claim-duplicate, attribute-canonicalization-failed, parser-profile-
    -   unsupported, 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.
    +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
    @@ -2113,23 +2289,23 @@ Internet-Draft                  HTMLTrust                    August 2026
        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.
    -
     
     
     
     
    -Grey                    Expires 28 February 2027               [Page 38]
    +Grey                    Expires 28 February 2027               [Page 41]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    +   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
    @@ -2173,15 +2349,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -
    -
    -
    -
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 39]
    +Grey                    Expires 28 February 2027               [Page 42]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2210,7 +2378,7 @@ Internet-Draft                  HTMLTrust                    August 2026
            | Concurrent verifier fetches per document    |          4 |
            +---------------------------------------------+------------+
     
    -                                 Table 4
    +                                 Table 5
     
        The limits apply independently to source verification, key
        resolution, and optional directory consultation.  A verifier that
    @@ -2237,7 +2405,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 40]
    +Grey                    Expires 28 February 2027               [Page 43]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2258,19 +2426,23 @@ Internet-Draft                  HTMLTrust                    August 2026
           with expires.
     
        Replay across origins:  An adversary copying a signed section from
    -      one site to another.  Prevented by origin binding (Section 5);
    -      verification on the new origin will fail.
    -
    -   Replay within origin (long-tail):  An adversary re-publishing old
    -      signed content on the same origin out of context.  Not prevented
    -      by the protocol; mitigated by external research and by the
    -      inclusion of signed-at in the signing payload, which dates the
    -      attestation.
    +      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 the verifier's right to refuse algorithms
    -      (Section 7.3) and by the requirement that algorithm mismatch
    -      produce a distinct failure outcome.
    +      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
     
    @@ -2282,24 +2454,24 @@ Internet-Draft                  HTMLTrust                    August 2026
        HTMLTrust system addresses these residual risks through layered
        means:
     
    -   *  Origin binding (Section 5) ensures that the signed material
    -      appears only on the publication origin under the original
    -      signature.
    +   *  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.
     
     
     
     
    -Grey                    Expires 28 February 2027               [Page 41]
    +Grey                    Expires 28 February 2027               [Page 44]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
    -   *  Future revisions MAY extend hash coverage to more semantic
    -      attributes, addressing a larger subset of context-swapping
    +   *  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-
    @@ -2345,11 +2517,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 42]
    +Grey                    Expires 28 February 2027               [Page 45]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2405,7 +2573,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 43]
    +Grey                    Expires 28 February 2027               [Page 46]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2461,7 +2629,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 44]
    +Grey                    Expires 28 February 2027               [Page 47]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2517,7 +2685,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 45]
    +Grey                    Expires 28 February 2027               [Page 48]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2573,7 +2741,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 46]
    +Grey                    Expires 28 February 2027               [Page 49]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2629,7 +2797,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 47]
    +Grey                    Expires 28 February 2027               [Page 50]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2685,7 +2853,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 48]
    +Grey                    Expires 28 February 2027               [Page 51]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2741,7 +2909,7 @@ Internet-Draft                  HTMLTrust                    August 2026
     
     
     
    -Grey                    Expires 28 February 2027               [Page 49]
    +Grey                    Expires 28 February 2027               [Page 52]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
    @@ -2765,15 +2933,18 @@ Appendix A.  Acknowledgements
     
     Appendix B.  Test Vectors
     
    -   This appendix gives a complete, reproducible end-to-end vector.  The
    -   same vector is maintained machine-readable in the conformance suite
    -   at conformance/vectors/vector-01.json in the canonicalization
    -   repository at commit a756dbaf27fd32799da7f2a1276cc16964e8d41f
    -   (https://github.com/HTMLTrust/htmltrust-canonicalization), where
    -   every signer and verifier binding reproduces it in CI.  All hashes
    -   are SHA-256 encoded as unpadded standard Base64 with the sha256:
    -   prefix (Section 6); the signature is Ed25519 over the Section 5
    -   payload, also unpadded standard Base64.
    +   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)
     
    @@ -2794,27 +2965,31 @@ public key (hex): ae8e474e7921dd51be650dcf6847ab452dee63421339efc3d6b041b2e85f4c
     
     
     
    -
    -
    -
    -Grey                    Expires 28 February 2027               [Page 50]
    +Grey                    Expires 28 February 2027               [Page 53]
     
     Internet-Draft                  HTMLTrust                    August 2026
     
     
     B.2.  Inputs
     
    -   Signed document base URL (for resolving relative signed URL
    -   attributes): https://example.com/essays/engines.  Serialized
    -   publisher origin (domain field): https://example.com. signed-at:
    +   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):
     
    -   
    +   
        
        
    -   
    +   
        

    On Analytical Engines

    The engine weaves algebraic patterns — just as the loom weaves flowers.

    See the notes and Portrait of Ada.

    @@ -2843,32 +3018,49 @@ B.4. Canonical claims Sorted lexically by the UTF-8 bytes of the normalized name: - author:Ada Lovelace - 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: - -Grey Expires 28 February 2027 [Page 51] +Grey Expires 28 February 2027 [Page 54] Internet-Draft HTMLTrust August 2026 - sha256:LRDw8hP30g9AxXzw8wteFOZpDyD/hWUyP25Be8C0hBs + 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 payload (Section 5, four colon-joined fields): + 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" + } -sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8:sha256:LRDw8hP30g9AxXzw8wteFOZpDyD/hWUyP25Be8C0hBs:https://example.com:2026-01-15T12:00:00Z + 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: -kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg +m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA Appendix C. Example Directory Exchange @@ -2876,44 +3068,43 @@ C.1. Submitting a content record Request: + + + + + + + + + +Grey Expires 28 February 2027 [Page 55] + +Internet-Draft HTMLTrust August 2026 + + 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="did:web:author.example";alg="ed25519" +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": "did:web:author.example", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg", + "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":"license","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] + "claims": [{"name":"author","content":"Ada Lovelace"},{"name":"claim:License","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] } Response: - - - - - - - - - - - - - -Grey Expires 28 February 2027 [Page 52] - -Internet-Draft HTMLTrust August 2026 - - HTTP/1.1 201 Created Location: https://directory.example/content/sha256%3AIVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8 Content-Type: application/htmltrust-content+json @@ -2923,10 +3114,13 @@ Content-Type: application/htmltrust-content+json "firstSeen": "2026-05-15T12:34:56Z", "signers": [ { - "keyid": "did:web:author.example", + "profile": "htmltrust-signature-v1", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg" + "scope": "url", + "location": "https://example.com/essays/engines", + "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA" } ], "endorsementCount": 0 @@ -2936,6 +3130,14 @@ C.2. Submitting an endorsement Request body (after canonicalization per Section 11.2 and signing): + + + +Grey Expires 28 February 2027 [Page 56] + +Internet-Draft HTMLTrust August 2026 + + { "endorser": "did:web:reviewer.example", "endorsement": "sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8", @@ -2961,30 +3163,15 @@ Appendix D. Open Issues addressed in subsequent revisions or in the companion W3C Community Group Report. - - - - -Grey Expires 28 February 2027 [Page 53] - -Internet-Draft HTMLTrust August 2026 - - - 1. Semantic attribute coverage. This revision signs canonicalized - text plus the provisional semantic attribute list href, src, alt, - and aria-label. The exact expansion list remains open for - community feedback. See Section 4.3.2 and the security - considerations. - - 2. Runtime DOM mutation. A data-htmltrust-ignore opt-out marker is + 1. Runtime DOM mutation. A data-htmltrust-ignore opt-out marker is reserved but not yet normative. See Section 4.7. - 3. Mandatory-to-implement key resolution methods. This revision + 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. - 4. Reputation-score interoperability. The /signers/{id}/reputation + 3. Reputation-score interoperability. The /signers/{id}/reputation endpoint is intentionally directory-specific. A future revision may define a small interoperable subset. @@ -3002,23 +3189,4 @@ Author's Address - - - - - - - - - - - - - - - - - - - -Grey Expires 28 February 2027 [Page 54] +Grey Expires 28 February 2027 [Page 57] diff --git a/ietf-draft/draft-grey-htmltrust-00.xml b/ietf-draft/draft-grey-htmltrust-00.xml index 44e958f..892e3e8 100644 --- a/ietf-draft/draft-grey-htmltrust-00.xml +++ b/ietf-draft/draft-grey-htmltrust-00.xml @@ -80,8 +80,9 @@ 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 the publication origin and to a -resolvable signer identifier. Verifiers re-derive the canonical text, +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. @@ -242,9 +243,24 @@ defined in Section 11.
    Origin:
    - The Web origin at which signed content is canonically published, as -defined by : the tuple of scheme, host, and port. HTMLTrust -serializes origins as described in Section 5. + A Web tuple of scheme, host, and port as defined by . 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.
    @@ -264,9 +280,9 @@ authoring tool).
    Publisher origin:
    Serves the resulting HTML to verifiers over the Web. The publisher -origin is bound into the signing payload via the legacy-named domain -field (Section 5). The publisher origin and the signer MAY be different -entities; the origin attests transport, the signer attests authorship. +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:
    @@ -294,8 +310,8 @@ the content hash. per Section 4.6, and computes the claims hash. The signer constructs the signing payload per Section 5 and signs it with the private key corresponding to the signer's key identifier. - The signer emits the HTML region annotated with key identifier, -signature, content hash, and algorithm attributes per . + The signer emits the HTML region annotated with the six required v1 +attributes defined in Section 5.1 and . The publisher origin serves the resulting HTML. A verifier obtains the HTML, extracts the signed section's attributes and content, and performs the procedure in Section 12. @@ -334,7 +350,7 @@ 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 -origin in the signing payload is derived from the signed document URL, not +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 @@ -450,8 +466,9 @@ tags do not themselves contribute bytes, but they MAY contribute boundaries per Section 4.5 and their descendant text nodes contribute to the canonical content. -The following signed semantic attributes also contribute to the -canonical content when present on an included element: +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: href @@ -460,9 +477,9 @@ canonical content when present on an included element: aria-label -This first signed-attribute list is intentionally small and is open -for community feedback. Future revisions MAY add attributes to the -list, but verifiers for this revision MUST use exactly the list above. +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 @@ -482,10 +499,10 @@ U+000A, so consecutive attribute records need no additional separator. 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 -parsing the attribute value as a URL against the signed document's base -URL and serializing the resulting URL using the Web URL serializer. If -URL parsing fails, verification MUST fail with -"attribute-canonicalization-failed". The normalized value MUST NOT +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 @@ -500,11 +517,9 @@ escaping. contributes @@attr:a:href:x, while a real href attribute contributes @attr:a:href:x. The two byte sequences therefore remain distinguishable. -Editor's Note: The precise expansion list for signed semantic -attributes is expected to evolve. Candidate additions include title, -cite, image dimension attributes, and ARIA attributes used in name -computation, but this revision deliberately avoids full structural HTML -signing. +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.
    Boundary-producing elements @@ -682,6 +697,11 @@ signed section, and verifiers MUST fail with "claim-duplicate" if 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 @@ -707,84 +727,233 @@ explicit experimental mode.
    Signing Payload Binding -The signature is computed over the deterministic UTF-8 byte string -formed by joining four fields with a single ASCII colon (U+003A) -between each: +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. + +
    Profile identifiers and wire attributes + +The identifiers fixed by this profile are: + + + 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. + +
    +
    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. + +
    +
    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. + +
    +
    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:
    -The fields are defined as follows. +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 . 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 time-stamp token, an append-only log entry, or +a trusted directory first-seen record. Such evidence is evaluated outside +the v1 signing payload. + +
    +
    Canonical signing object + +The signer constructs this JSON object, with every member present: + +
    + +The members are defined as follows:
    -
    content-hash:
    +
    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 content hash as defined in Section 6.2, including the -algorithm prefix and separator (for example, -sha256:Zm9vYmFy...). Verifiers MUST use the literal value -serialized into the content-hash attribute of the signed section -element. + 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.
    -
    claims-hash:
    +
    contentHash:
    - The claims hash, formatted identically to content-hash. The -claims hash is not carried in a separate attribute on the wire; it -is recomputed by the verifier from the canonical claims byte -string (Section 4.6) using the algorithm given by the prefix of -the content-hash attribute. The hash algorithm for both -content-hash and claims-hash MUST be the same. + The exact content-hash attribute value after its encoding has been +validated as canonical under Section 6.
    -
    domain:
    +
    context:
    - A legacy field name retained for wire compatibility. Its value is -the serialized Web origin at which the content is canonically -published. The value MUST be the result of the URL Standard's origin -serialization algorithm for the document URL, with no path, query, -fragment, or credentials. Conforming public signatures MUST use an -HTTPS tuple origin. An HTTP tuple origin MAY be enabled by an explicitly -configured local-development profile, but that profile MUST be -distinguishable from normal verification and MUST NOT be accepted for -public content by default. Opaque origins (data:, file:, and -similar) are invalid. The URL Standard, rather than a host-language URL -serializer, defines lowercasing, IDNA A-label conversion, IPv6 bracket -syntax, and default port omission. For example, https://example.org -and https://example.org:8443 are distinct origins. + The exact ASCII string https://htmltrust.org/protocol/signed-section. +This member separates HTMLTrust signed-section signatures from signatures +made for another protocol.
    -
    signed-at:
    +
    keyid:
    - The value of the signed-at claim <meta> element, after the -normalization in Section 4.4 has been applied. The value MUST -conform to the date-time production of with the time -offset given in UTC ("Z"). + 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 fields MUST appear in the order shown. The separator MUST be a -single U+003A COLON. No leading or trailing whitespace is permitted. +The signing payload is the UTF-8 encoding of this object serialized with the +JSON Canonicalization Scheme . The serialized object contains no +unknown or optional members in v1. The signature operation in Section 6.3 is +applied directly to those bytes. + +
    +
    Algorithm and signer binding -
    Identity not bound directly +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. -The signer's identifier (keyid) is intentionally not included in -the signing payload. The identifier is implicit in key resolution -(Section 8): any attempt to claim a different identifier would -resolve to a different public key, and the signature would fail to -verify. Including the identifier explicitly would add a fragile -string-matching surface without strengthening the cryptographic -binding. +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.
    -
    Why the publisher origin +
    Legacy payloads -Binding the signature to the publication origin prevents verbatim -re-publication of signed content with the original signature -preserved. A reader who encounters apparently signed content at a -different origin will see a signature verification failure caused by -the origin mismatch alone, regardless of any other property of the -content. Legitimate re-publication is supported by a separate -wrapper-signing mechanism using a claim:CanonicalURL field (see -); the wrapper signer attests to a republication, not -to original authorship, and is responsible for the chain. +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.
    @@ -1217,18 +1386,14 @@ capabilities. The response media type is "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, and -supportedAlgorithms are REQUIRED. Other fields are OPTIONAL. - -JSON members named domain in this API carry the serialized origin -value defined for the signing payload in Section 5. The field name is -retained for compatibility with early tooling and MUST NOT be -interpreted as a host-only domain name. +The fields directory, version, capabilities, supportedAlgorithms, and +supportedProfiles are REQUIRED. Other fields are OPTIONAL.
    GET /content/{hash} @@ -1246,9 +1411,12 @@ any / in the Base64 value becomes
    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 @@ -1542,9 +1719,9 @@ endorsements.
    Verification Procedure A conforming verifier MUST perform the following steps in order for -each signed section it intends to verify. Steps 11.1 through 11.6 +each signed section it intends to verify. Steps 12.1 through 12.6 are the cryptographic verification ("layer 1" in ). -Step 11.7 is the trust decision and is out of scope for this +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. @@ -1557,10 +1734,16 @@ 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 four required attributes keyid, signature, -content-hash, and algorithm and the inner content of the signed -section. If any required attribute is missing, the verifier MUST -return an "incomplete" outcome and stop. +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 @@ -1582,7 +1765,8 @@ 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. +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. @@ -1597,25 +1781,27 @@ 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. +
    Step 4: Construct signing payload -Construct the byte string defined in Section 5 from content-hash -(verbatim from the attribute), the claims hash from Step 3, the -serialized origin of the current document (the legacy-named domain -field in Section 5), and the value -of the signed-at claim <meta> element after normalization -(Section 4.4). If the signed-at claim is absent, the verifier MUST -return a "claim-missing" failure and stop. +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. -If the normalized signed-at value is not an RFC 3339 date-time with the -UTC Z offset required by Section 5, the verifier MUST return -"claim-malformed" and stop. +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 , then UTF-8 encode +the result. Those bytes are the signing payload. -If the document URL does not produce an allowed tuple origin under Section 5, -the verifier MUST return "origin-not-supported" and stop. A signature copied -to another allowed origin fails cryptographic verification because the -verifier constructs the payload from the current document origin. +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.
    Step 5: Resolve keyid @@ -1662,9 +1848,12 @@ revision. A verifier MAY attach private diagnostic detail, but th cryptographic result MUST use one of these identifiers and MUST never map a listed failure to valid: -incomplete, content-hash-mismatch, +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, @@ -1809,23 +1998,24 @@ cannot prevent forgery in this case. Mitigations are revocation
    Replay across origins:
    - An adversary copying a signed section from one site to another. -Prevented by origin binding (Section 5); verification on the new -origin will fail. + 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 (long-tail):
    +
    Replay within origin:
    - An adversary re-publishing old signed content on the same origin -out of context. Not prevented by the protocol; mitigated by -external research and by the inclusion of signed-at in the -signing payload, which dates the attestation. + 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 the verifier's right to refuse algorithms (Section 7.3) and by -the requirement that algorithm mismatch produce a distinct -failure outcome. +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.
    @@ -1840,13 +2030,13 @@ hostile media without invalidating the cryptographic signature. The HTMLTrust system addresses these residual risks through layered means: - Origin binding (Section 5) ensures that the signed material appears -only on the publication origin under the original signature. + 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 extend hash coverage to more semantic -attributes, addressing a larger subset of context-swapping attacks. + 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 @@ -2545,7 +2735,7 @@ author and the Community Group. - +
    Acknowledgements @@ -2560,14 +2750,16 @@ the operational experience of DKIM deployment.
    Test Vectors -This appendix gives a complete, reproducible end-to-end vector. The same -vector is maintained machine-readable in the conformance suite at -conformance/vectors/vector-01.json in the canonicalization repository at -commit a756dbaf27fd32799da7f2a1276cc16964e8d41f -(https://github.com/HTMLTrust/htmltrust-canonicalization), where every -signer and verifier binding reproduces it in CI. All hashes are SHA-256 -encoded as unpadded standard Base64 with the sha256: prefix -(Section 6); the signature is Ed25519 over the Section 5 payload, also +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 +, 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.
    Test key (Ed25519) @@ -2591,18 +2783,25 @@ MCowBQYDK2VwAyEAro5HTnkh3VG+ZQ3PaEerRS3uY0ITOe/D1rBBsuhfTBk=
    Inputs -Signed document base URL (for resolving relative signed URL attributes): -https://example.com/essays/engines. Serialized publisher origin -(domain field): https://example.com. signed-at: -2026-01-15T12:00:00Z. +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):
    + - +

    On Analytical Engines

    The engine weaves algebraic patterns — just as the loom weaves flowers.

    See the notes and Portrait of Ada.

    @@ -2640,30 +2839,49 @@ sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8
    (The serialized string ends with a trailing U+000A after the last record.) Claims hash:
    Signing payload and signature -Signing payload (Section 5, four colon-joined fields): +Signing object before JCS serialization: -
    +
    + +JCS signing payload, shown on one line: + +
    Ed25519 signature over that payload, unpadded standard Base64:
    @@ -2679,18 +2897,21 @@ 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="did:web:author.example";alg="ed25519" +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": "did:web:author.example", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg", + "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":"license","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] + "claims": [{"name":"author","content":"Ada Lovelace"},{"name":"claim:License","content":"CC-BY-4.0"},{"name":"signed-at","content":"2026-01-15T12:00:00Z"}] } ]]> @@ -2706,10 +2927,13 @@ Content-Type: application/htmltrust-content+json "firstSeen": "2026-05-15T12:34:56Z", "signers": [ { - "keyid": "did:web:author.example", + "profile": "htmltrust-signature-v1", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", "signedAt": "2026-01-15T12:00:00Z", - "domain": "https://example.com", - "signature": "kivLG5v5apFvXL4R7glxzg6zqpAt9r7gbiKRK3EA0W3wLePdn/Jde2lIdD1a78TdsaxZyRgDF/6u/goO6NcKDg" + "scope": "url", + "location": "https://example.com/essays/engines", + "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA" } ], "endorsementCount": 0 @@ -2753,11 +2977,6 @@ be addressed in subsequent revisions or in the companion W3C Community Group Report.
    - Semantic attribute coverage. This revision signs canonicalized text -plus the provisional semantic attribute list href, src, alt, -and aria-label. The exact expansion list remains open for -community feedback. See Section 4.3.2 and the security -considerations. Runtime DOM mutation. A data-htmltrust-ignore opt-out marker is reserved but not yet normative. See Section 4.7. Mandatory-to-implement key resolution methods. This revision @@ -2775,680 +2994,745 @@ may define a small interoperable subset. diff --git a/ietf-draft/vectors/signing-profile-v1.json b/ietf-draft/vectors/signing-profile-v1.json new file mode 100644 index 0000000..887837a --- /dev/null +++ b/ietf-draft/vectors/signing-profile-v1.json @@ -0,0 +1,232 @@ +{ + "description": "HTMLTrust signing profile v1 vector from canonical content and normalized claim records", + "profile": { + "signature": "htmltrust-signature-v1", + "canonicalization": "htmltrust-c14n-v1", + "attributes": "htmltrust-attrs-v1", + "url": "htmltrust-safe-url-v1", + "context": "https://htmltrust.org/protocol/signed-section" + }, + "key": { + "seedHex": "68746d6c74727573742d746573742d766563746f722d656432353531392d3031", + "publicKeyRawHex": "ae8e474e7921dd51be650dcf6847ab452dee63421339efc3d6b041b2e85f4c19" + }, + "input": { + "documentURL": "HTTPS://EXAMPLE.COM:443/essays/engines#analysis", + "attributes": { + "profile": "htmltrust-signature-v1", + "signature-scope": "url", + "keyid": "https://keys.example/alice-2026.json", + "algorithm": "ed25519", + "content-hash": "sha256:IVAwpRTDujszmYf76W497alVTtxGCgtJtQlasiFSCM8" + }, + "canonicalContent": "On Analytical Engines\nThe engine weaves algebraic patterns - just as the loom weaves flowers.\nSee @attr:a:href:https://example.com/notes/engine\nthe notes and @attr:img:src:https://example.com/img/ada.png\n@attr:img:alt:Portrait of Ada\n.", + "normalizedClaims": [ + { + "name": "author", + "content": "Ada Lovelace" + }, + { + "name": "claim:License", + "content": "CC-BY-4.0" + }, + { + "name": "signed-at", + "content": "2026-01-15T12:00:00Z" + } + ], + "canonicalClaims": "author:Ada Lovelace\nclaim\\:License:CC-BY-4.0\nsigned-at:2026-01-15T12\\:00\\:00Z\n", + "signedAt": "2026-01-15T12:00:00Z" + }, + "claimsHash": "sha256:Fk5udwCnu1au8v5oaBsU+aSB5S2zSLqoF0xXO6HrIn4", + "signingObject": { + "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": "{\"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\"}", + "signature": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA", + "tamperFields": [ + "algorithm", + "attributeProfile", + "canonicalizationProfile", + "claimsHash", + "contentHash", + "context", + "keyid", + "location", + "profile", + "scope", + "signedAt", + "urlProfile" + ], + "signatureCases": [ + { + "value": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA", + "valid": true + }, + { + "value": "m0ykSPqUWdyZprUAqosOB2IEK2XsKp7auPIWz80/2ht+LwT1LiNcsLL6cn2IkmTZFG9ptLiUHaB1crPJgBw7BA=", + "failure": "invalid-encoding" + }, + { + "value": "***", + "failure": "invalid-encoding" + }, + { + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "failure": "malformed-signature" + }, + { + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "failure": "signature-invalid" + } + ], + "locationCases": [ + { + "documentURL": "HTTPS://BÜCHER.EXAMPLE:443/a/../article?q=1#part", + "scope": "url", + "location": "https://xn--bcher-kva.example/article?q=1" + }, + { + "documentURL": "https://example.org:8443/a?q=1#part", + "scope": "origin", + "location": "https://example.org:8443" + }, + { + "documentURL": "https://example.org:8443/completely/different?query=2", + "scope": "origin", + "location": "https://example.org:8443" + }, + { + "documentURL": "https://user@example.org/article", + "scope": "url", + "failure": "origin-not-supported" + }, + { + "documentURL": "http://example.org/article", + "scope": "url", + "failure": "origin-not-supported" + }, + { + "documentURL": "file:///home/example/article.html", + "scope": "origin", + "failure": "origin-not-supported" + }, + { + "documentURL": "not a URL", + "scope": "url", + "failure": "origin-not-supported" + }, + { + "documentURL": "https://example.org/article", + "scope": "path", + "failure": "scope-unsupported" + } + ], + "timestampCases": [ + { + "value": "2024-02-29T23:59:59Z", + "valid": true + }, + { + "value": "2023-02-29T23:59:59Z", + "failure": "timestamp-invalid" + }, + { + "value": "2026-01-15T12:00:60Z", + "failure": "timestamp-invalid" + }, + { + "value": "2026-01-15T12:00:00.000Z", + "failure": "timestamp-invalid" + }, + { + "value": "2026-01-15t12:00:00z", + "failure": "timestamp-invalid" + }, + { + "value": "2026-01-15T07:00:00-05:00", + "failure": "timestamp-invalid" + }, + { + "value": "0000-01-15T12:00:00Z", + "failure": "timestamp-invalid" + }, + { + "value": "2026-13-15T12:00:00Z", + "failure": "timestamp-invalid" + } + ], + "urlPolicyCases": [ + { + "baseURL": "https://example.org/articles/one", + "value": "../notes?q=1#proof", + "url": "https://example.org/notes?q=1#proof" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "javascript:alert(1)", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "data:text/plain,hello", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "blob:https://example.org/1234", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "file:///home/example/report.pdf", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "mailto:alice@example.org", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "tel:+15551234567", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "web+example:opaque", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "http://example.org/plaintext", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "https://user:secret@example.org/private", + "failure": "url-policy-violation" + }, + { + "baseURL": "https://example.org/articles/one", + "value": "https://example.org/line\nbreak", + "failure": "url-policy-violation" + }, + { + "baseURL": "not a URL", + "value": "relative", + "failure": "attribute-canonicalization-failed" + } + ] +} diff --git a/scripts/check-vectors.mjs b/scripts/check-vectors.mjs index 4e14d50..9fb68b7 100644 --- a/scripts/check-vectors.mjs +++ b/scripts/check-vectors.mjs @@ -10,20 +10,127 @@ const load = (name) => JSON.parse( readFileSync(new URL(`../ietf-draft/vectors/${name}`, import.meta.url), "utf8"), ); +const assertUnicodeScalarString = (value) => { + for (let index = 0; index < value.length; index += 1) { + const codeUnit = value.charCodeAt(index); + if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { + const next = value.charCodeAt(index + 1); + if (!(next >= 0xdc00 && next <= 0xdfff)) { + throw new Error("JCS input contains an unpaired high surrogate"); + } + index += 1; + } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { + throw new Error("JCS input contains an unpaired low surrogate"); + } + } +}; + +const canonicalizeJcs = (value) => { + if (value === null) return "null"; + if (typeof value === "boolean") return value ? "true" : "false"; + if (typeof value === "string") { + assertUnicodeScalarString(value); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) throw new Error("JCS numbers must be finite"); + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return `[${value.map(canonicalizeJcs).join(",")}]`; + } + if (typeof value === "object") { + const names = Object.keys(value).sort((left, right) => ( + left < right ? -1 : left > right ? 1 : 0 + )); + return `{${names.map((name) => { + assertUnicodeScalarString(name); + return `${JSON.stringify(name)}:${canonicalizeJcs(value[name])}`; + }).join(",")}}`; + } + throw new Error(`JCS does not support ${typeof value}`); +}; + +const privateKeyFromSeed = (seedHex) => { + const seed = Buffer.from(seedHex, "hex"); + const pkcs8Prefix = Buffer.from("302e020100300506032b657004220420", "hex"); + return createPrivateKey({ + key: Buffer.concat([pkcs8Prefix, seed]), + format: "der", + type: "pkcs8", + }); +}; + +const unpaddedBase64 = (buffer) => buffer.toString("base64").replace(/=+$/, ""); + +const SIGNING_V1 = Object.freeze({ + signature: "htmltrust-signature-v1", + canonicalization: "htmltrust-c14n-v1", + attributes: "htmltrust-attrs-v1", + url: "htmltrust-safe-url-v1", + context: "https://htmltrust.org/protocol/signed-section", +}); + +const parsePrefixedHash = (value) => { + const match = /^(sha256|sha384|sha512):([A-Za-z0-9+/]+)$/.exec(value); + if (!match) throw new Error(`invalid prefixed hash ${value}`); + const [, algorithm, encoded] = match; + const bytes = Buffer.from(encoded, "base64"); + const expectedLength = { sha256: 32, sha384: 48, sha512: 64 }[algorithm]; + if (bytes.length !== expectedLength || unpaddedBase64(bytes) !== encoded) { + throw new Error(`non-canonical prefixed hash ${value}`); + } + return { algorithm, bytes }; +}; + +const parseEd25519Signature = (value) => { + if (!/^[A-Za-z0-9+/]+$/.test(value)) throw new Error("invalid-encoding"); + const bytes = Buffer.from(value, "base64"); + if (unpaddedBase64(bytes) !== value) throw new Error("invalid-encoding"); + if (bytes.length !== 64) throw new Error("malformed-signature"); + return bytes; +}; + +const escapeClaimField = (value) => value + .replaceAll("\\", "\\\\") + .replaceAll(":", "\\:") + .replaceAll("\n", "\\n"); + +const serializeNormalizedClaims = (claims) => { + if (!Array.isArray(claims)) throw new Error("normalized claims must be an array"); + const seen = new Set(); + const records = claims.map(({ name, content }) => { + if (typeof name !== "string" || typeof content !== "string" || name.length === 0) { + throw new Error("normalized claim is malformed"); + } + assertUnicodeScalarString(name); + assertUnicodeScalarString(content); + if (seen.has(name)) throw new Error(`duplicate normalized claim ${name}`); + seen.add(name); + return { name, content }; + }); + records.sort((left, right) => Buffer.compare( + Buffer.from(left.name, "utf8"), + Buffer.from(right.name, "utf8"), + )); + return records.map(({ name, content }) => ( + `${escapeClaimField(name)}:${escapeClaimField(content)}\n` + )).join(""); +}; + const endorsement = load("endorsement-01.json"); -const payload = Buffer.from(endorsement.jcsWithoutSignature, "utf8"); +const { signature: endorsementSignature, ...unsignedEndorsement } = endorsement.document; +const endorsementJcs = canonicalizeJcs(unsignedEndorsement); +if (endorsementJcs !== endorsement.jcsWithoutSignature) { + throw new Error("endorsement-01 JCS serialization does not match"); +} +const payload = Buffer.from(endorsementJcs, "utf8"); const digest = createHash("sha256").update(payload).digest("base64").replace(/=+$/, ""); if (`sha256:${digest}` !== endorsement.jcsSha256) { throw new Error("endorsement-01 JCS hash does not match"); } -const seed = Buffer.from(endorsement.key.seedHex, "hex"); -const pkcs8Prefix = Buffer.from("302e020100300506032b657004220420", "hex"); -const privateKey = createPrivateKey({ - key: Buffer.concat([pkcs8Prefix, seed]), - format: "der", - type: "pkcs8", -}); +const privateKey = privateKeyFromSeed(endorsement.key.seedHex); const publicKey = createPublicKey(privateKey); const publicKeyRaw = publicKey.export({ format: "der", type: "spki" }).subarray(-32); if (publicKeyRaw.toString("hex") !== endorsement.key.publicKeyRawHex) { @@ -33,7 +140,7 @@ if (!verify( null, payload, publicKey, - Buffer.from(endorsement.document.signature, "base64"), + Buffer.from(endorsementSignature, "base64"), )) { throw new Error("endorsement-01 signature does not verify"); } @@ -61,3 +168,194 @@ for (const testCase of load("origin-serialization.json").cases) { throw new Error(`opaque origin was accepted for ${testCase.inputURL}`); } } + +const strictTimestamp = (value) => { + const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/.exec(value); + if (!match) return false; + const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match; + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + const hour = Number(hourText); + const minute = Number(minuteText); + const second = Number(secondText); + if (year < 1 || month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) { + return false; + } + const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + return day >= 1 && day <= days[month - 1]; +}; + +const deriveLocation = (documentURL, scope) => { + if (scope !== "url" && scope !== "origin") { + throw new Error("scope-unsupported"); + } + let url; + try { + url = new URL(documentURL); + } catch { + throw new Error("origin-not-supported"); + } + if (url.protocol !== "https:" || url.username || url.password || url.origin === "null") { + throw new Error("origin-not-supported"); + } + if (scope === "origin") return url.origin; + url.hash = ""; + return url.href; +}; + +const safeUrl = (value, baseURL) => { + if (/[\u0000-\u001f\u007f]/u.test(value)) { + throw new Error("url-policy-violation"); + } + let url; + try { + url = new URL(value, baseURL); + } catch { + throw new Error("attribute-canonicalization-failed"); + } + if (url.protocol !== "https:" || url.username || url.password) { + throw new Error("url-policy-violation"); + } + return url.href; +}; + +const signing = load("signing-profile-v1.json"); +for (const [name, expected] of Object.entries(SIGNING_V1)) { + if (signing.profile[name] !== expected) { + throw new Error(`signing-profile-v1 ${name} constant does not match v1`); + } +} +if (signing.input.attributes.profile !== SIGNING_V1.signature) { + throw new Error("signing-profile-v1 wire profile does not match v1"); +} +if (signing.input.attributes.algorithm !== "ed25519") { + throw new Error("signing-profile-v1 vector requires ed25519"); +} +if (!strictTimestamp(signing.input.signedAt)) { + throw new Error("signing-profile-v1 primary timestamp is invalid"); +} +if (serializeNormalizedClaims(signing.input.normalizedClaims) + !== signing.input.canonicalClaims) { + throw new Error("signing-profile-v1 normalized claims do not match canonical claims"); +} +const signedAtClaims = signing.input.normalizedClaims + .filter(({ name }) => name === "signed-at"); +if (signedAtClaims.length !== 1 || signedAtClaims[0].content !== signing.input.signedAt) { + throw new Error("signing-profile-v1 signed-at bindings differ"); +} + +const parsedContentHash = parsePrefixedHash(signing.input.attributes["content-hash"]); +const parsedClaimsHash = parsePrefixedHash(signing.claimsHash); +if (parsedContentHash.algorithm !== parsedClaimsHash.algorithm) { + throw new Error("signing-profile-v1 content and claims algorithms differ"); +} +const signingContentDigest = unpaddedBase64( + createHash(parsedContentHash.algorithm).update(signing.input.canonicalContent, "utf8").digest(), +); +if (`${parsedContentHash.algorithm}:${signingContentDigest}` + !== signing.input.attributes["content-hash"]) { + throw new Error("signing-profile-v1 content hash does not match"); +} +const signingClaimsDigest = unpaddedBase64( + createHash(parsedClaimsHash.algorithm).update(signing.input.canonicalClaims, "utf8").digest(), +); +if (`${parsedClaimsHash.algorithm}:${signingClaimsDigest}` !== signing.claimsHash) { + throw new Error("signing-profile-v1 claims hash does not match"); +} + +const builtSigningObject = { + algorithm: signing.input.attributes.algorithm, + attributeProfile: SIGNING_V1.attributes, + canonicalizationProfile: SIGNING_V1.canonicalization, + claimsHash: signing.claimsHash, + contentHash: signing.input.attributes["content-hash"], + context: SIGNING_V1.context, + keyid: signing.input.attributes.keyid, + location: deriveLocation( + signing.input.documentURL, + signing.input.attributes["signature-scope"], + ), + profile: SIGNING_V1.signature, + scope: signing.input.attributes["signature-scope"], + signedAt: signing.input.signedAt, + urlProfile: SIGNING_V1.url, +}; +if (canonicalizeJcs(builtSigningObject) !== canonicalizeJcs(signing.signingObject)) { + throw new Error("signing-profile-v1 constructed object does not match"); +} +const signingJcs = canonicalizeJcs(builtSigningObject); +if (signingJcs !== signing.jcs) { + throw new Error("signing-profile-v1 JCS serialization does not match"); +} +const signingPrivateKey = privateKeyFromSeed(signing.key.seedHex); +const signingPublicKey = createPublicKey(signingPrivateKey); +if (signingPublicKey.export({ format: "der", type: "spki" }).subarray(-32).toString("hex") + !== signing.key.publicKeyRawHex) { + throw new Error("signing-profile-v1 public key does not match the seed"); +} +const signingSignature = parseEd25519Signature(signing.signature); +if (!verify( + null, + Buffer.from(signingJcs, "utf8"), + signingPublicKey, + signingSignature, +)) { + throw new Error("signing-profile-v1 signature does not verify"); +} +for (const field of signing.tamperFields) { + if (!(field in builtSigningObject)) { + throw new Error(`signing-profile-v1 unknown tamper field ${field}`); + } + const tampered = { ...builtSigningObject, [field]: `${builtSigningObject[field]}-tampered` }; + if (verify( + null, + Buffer.from(canonicalizeJcs(tampered), "utf8"), + signingPublicKey, + signingSignature, + )) { + throw new Error(`signing-profile-v1 did not bind ${field}`); + } +} + +for (const testCase of signing.signatureCases) { + try { + const candidate = parseEd25519Signature(testCase.value); + if (!verify(null, Buffer.from(signingJcs, "utf8"), signingPublicKey, candidate)) { + throw new Error("signature-invalid"); + } + if (!testCase.valid) throw new Error("unexpected valid signature"); + } catch (error) { + if (!testCase.failure || error.message !== testCase.failure) throw error; + } +} + +for (const testCase of signing.locationCases) { + try { + const location = deriveLocation(testCase.documentURL, testCase.scope); + if (testCase.failure || location !== testCase.location) { + throw new Error(`unexpected location result for ${testCase.documentURL}`); + } + } catch (error) { + if (!testCase.failure || error.message !== testCase.failure) throw error; + } +} + +for (const testCase of signing.timestampCases) { + const valid = strictTimestamp(testCase.value); + if (valid !== Boolean(testCase.valid)) { + throw new Error(`unexpected timestamp result for ${testCase.value}`); + } +} + +for (const testCase of signing.urlPolicyCases) { + try { + const url = safeUrl(testCase.value, testCase.baseURL); + if (testCase.failure || url !== testCase.url) { + throw new Error(`unexpected URL-policy result for ${testCase.value}`); + } + } catch (error) { + if (!testCase.failure || error.message !== testCase.failure) throw error; + } +} diff --git a/scripts/check-w3c.sh b/scripts/check-w3c.sh index 65ca5ff..6837a8f 100755 --- a/scripts/check-w3c.sh +++ b/scripts/check-w3c.sh @@ -8,6 +8,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" scratch_root="${TMPDIR:-${RUNNER_TEMP:-/var/tmp}}" npm_cache="$scratch_root/htmltrust-spec-npm-cache" snapshot="$scratch_root/htmltrust-w3c-snapshot.html" +respec_work="$scratch_root/htmltrust-respec-work" if ! command -v npx >/dev/null 2>&1; then echo "Missing npx. Install Node.js 22 or newer." >&2 @@ -34,14 +35,32 @@ if [[ -z "$chrome_path" || ! -x "$chrome_path" ]]; then fi mkdir -p "$npm_cache" -cd "$repo_root" +mkdir -p "$respec_work" + +# ReSpec's CLI intercepts its main browser script when --use-local is used, +# but a Web Worker then fetches the syntax highlighter cross-origin. Chromium +# rejects that fetch without CORS headers. Stage both pinned browser bundles +# beside a scratch copy of the source so the worker fetch remains same-origin. +respec_command="$({ + npm_config_cache="$npm_cache" npx --yes --package=respec@37.3.5 \ + -c 'command -v respec' +})" +respec_entry="$(node -e \ + 'console.log(require("node:fs").realpathSync(process.argv[1]))' \ + "$respec_command")" +respec_root="$(cd "$(dirname "$respec_entry")/.." && pwd)" +cp "$respec_root/builds/respec-w3c.js" "$respec_work/respec-w3c.js" +cp "$respec_root/builds/respec-highlight.js" "$respec_work/respec-highlight.js" +sed 's#https://www.w3.org/Tools/respec/respec-w3c#./respec-w3c.js#' \ + "$repo_root/w3c-cg/index.html" > "$respec_work/index.html" + +cd "$respec_work" npm_config_cache="$npm_cache" \ PUPPETEER_EXECUTABLE_PATH="$chrome_path" \ npx --yes respec@37.3.5 \ --localhost \ - --use-local \ --disable-sandbox \ --haltonerror \ --haltonwarn \ - w3c-cg/index.html \ + index.html \ "$snapshot" diff --git a/scripts/check.sh b/scripts/check.sh index b6a3187..f46492d 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -5,7 +5,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo_root" for vector in ietf-draft/vectors/*.json; do - jq empty "$vector" + node -e 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"))' "$vector" done node scripts/check-vectors.mjs diff --git a/w3c-cg/index.html b/w3c-cg/index.html index 08b79e0..de2e496 100644 --- a/w3c-cg/index.html +++ b/w3c-cg/index.html @@ -64,7 +64,7 @@ This document specifies HTMLTrust, an HTML extension that allows authors to cryptographically sign semantically meaningful regions of a web page using a new <signed-section> element. Signed sections - carry an author-resolvable key identifier, a signature, a content hash, a + carry the six wire attributes required by the selected signing profile, a required signed-at claim, and optional additional claims expressed as child <meta> elements. User agents verify these signatures locally and expose results through a @@ -203,12 +203,22 @@

    Categories and content model

    Attributes

    The following content attributes are defined on the - signed-section element. All four MUST + signed-section element. All six MUST be present on any element intended for verification; an element missing any required attribute MUST be treated as unverifiable (see § Verification lifecycle).

    +
    profile (required)
    +
    The exact, case-sensitive signing-profile identifier. This report + defines htmltrust-signature-v1. An unknown value fails + with profile-unsupported; user agents must not retry a + failed verification under a legacy profile.
    +
    signature-scope (required)
    +
    Either url or origin. URL scope binds the + signature to the final response URL after its fragment is removed. + Origin scope allows the same signature to verify at another URL on + the same HTTPS origin. Authoring tools should emit URL scope.
    keyid (required)
    A string identifying the signer. The value is opaque to the HTML @@ -243,9 +253,20 @@

    Attributes

    any global attributes. The semantic attributes href, src, alt, and aria-label are covered by the signature when present on included descendants, as - defined in [[I-D.grey-htmltrust]]. Other global attributes are not - covered by the signature in this revision; see - Issue 1. + defined by the fixed htmltrust-attrs-v1 profile in + [[I-D.grey-htmltrust]]. Other global attributes are not covered by the + signature in this revision. +

    +

    + Signed href and src values use the + htmltrust-safe-url-v1 policy. The policy consumes the + accepted HTML parser's decoded attribute value. User agents inspect + that value for controls before URL parsing, then resolve it against the + source snapshot's document base URL. The result must be an HTTPS URL + without credentials. Other schemes fail with + url-policy-violation; parse errors fail with + attribute-canonicalization-failed. Verification does not + fetch these signed URLs.

    Canonicalization escapes U+0040 in text and signed attribute values @@ -287,18 +308,23 @@

    In-band claim metadata

    participates in the claims hash. The protocol assigns no identity semantics to the string by itself.
    signed-at
    -
    An ISO 8601 timestamp [[ISO8601]] indicating when the signature - was produced. This value participates in the canonical signing +
    A signer-asserted UTC timestamp in the exact + YYYY-MM-DDTHH:MM:SSZ form defined by + [[I-D.grey-htmltrust]]. It participates in the canonical signing payload defined in [[I-D.grey-htmltrust]], is also included in the canonical claims hash like every other direct child claim, and - MUST be present for a signature to verify.
    + MUST be present for a signature to verify. + It proves that the value was signed; independent timestamp evidence + is needed to prove when the signature existed.
    claim:License
    An SPDX license identifier or URL describing the license under which the signed content is offered. Its normalized value participates in the claims hash; the protocol does not validate the license.
    claim:CanonicalURL
    The URL at which this signed region is canonically published. - Used by republication wrappers; see [[I-D.grey-htmltrust]].
    + This is descriptive signed metadata. It does not select or replace + the location derived from the final response URL and + signature-scope.

    Claim <meta> elements MUST @@ -306,9 +332,13 @@

    In-band claim metadata

    and MUST precede any other flow content children of that element. Claims nested inside descendant elements are not treated as claims of the enclosing signed-section. + Normalized claim names are case-sensitive. Only the exact ASCII name + signed-at has protocol timestamp semantics.

    @@ -820,22 +886,25 @@

    Side channels

    -

    Republication and origin binding

    +

    Republication and location binding

    - The signing payload defined in [[I-D.grey-htmltrust]] binds a - signature to the origin at which the content is canonically - published. User agents MUST evaluate - origin binding as part of layer-1 verification. A document URL that - does not produce an allowed tuple origin + The signing object defined in [[I-D.grey-htmltrust]] binds a signature + to the final response URL or its origin, according to the signed + signature-scope. User agents + MUST evaluate location binding as part of + layer-1 verification. A document URL that does not produce an allowed + HTTPS location MUST produce origin-not-supported. Copying signed content to another - allowed origin changes the signing payload, so the copied signature - produces signature-invalid. + location outside the signed scope changes the signing object, so the + copied signature produces signature-invalid.

    - The IETF signing payload retains the legacy field name - domain, but that field carries the serialized Web origin: - scheme, host, and port. It is not a host-only domain string. + URL scope includes the serialized path and query and excludes the + fragment. It blocks same-origin replay at another URL. Origin scope is + an explicit portability choice and permits that replay. A canonical + link, document base URL, or page-supplied URL metadata cannot change + either location.

    @@ -972,16 +1041,7 @@

    Open Issues

    Community Group Draft status.

    -

    - [[I-D.grey-htmltrust]] now signs canonicalized text plus the - provisional semantic attribute list href, src, - alt, and aria-label. The Community Group invites - input on which additional attributes and element-specific cases should be - covered without drifting toward the full-structural signing that made XML - Digital Signatures [[XMLDSIG-CORE2]] operationally fragile. -

    - -

    +

    Verification prefers the original server HTML response snapshot, with a source refetch fallback when no snapshot is available. Common runtime decorations — copy-to-clipboard buttons injected into @@ -992,7 +1052,7 @@

    Open Issues

    excludes a region from canonicalization, paired with authoring guidance.

    -

    +

    User agents MUST accept at least one resolution method, but the spec does not yet mandate a preference order when a keyid would resolve under more than one @@ -1033,7 +1093,7 @@

    Conformance Summary

    A conforming authoring tool MUST:

      -
    • Emit signed-section elements that include all four +
    • Emit signed-section elements that include all six required attributes.
    • Place claim <meta> elements as direct children of the signed-section element, before any other flow

    Table 5
    Resource