spec(brand): add response signing envelope#5192
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Approving. The envelope shape, tenant binding, request-hash composition, and iat/exp freshness rules are coherent with the prior #4703 consistency PR and the shared #adcp-jws-profile, and minor is the right changeset bump because verify_brand_claim/verify_brand_claims are pre-release 3.1 — adding a required field before GA is not a breaking flip.
Things I checked
- Per-task
constonsigned_response.payload.taskmatches the envelope-leveltaskenum:verify_brand_claimandverify_brand_claimsagree on both sides.static/schemas/source/brand/verify-brand-claim-response.json:117andverify-brand-claims-response.json:99. signed_success_payload.not.anyOfcorrectly excludes botherrorsandsigned_response— the envelope cannot recurse under its own attested payload and the success arm cannot impersonate the error arm. Load-bearing.request_hashpattern^sha256:[A-Za-z0-9_-]{43}$matches unpadded base64url of a 32-byte digest. Right shape.static/schemas/source/index.json:349correctly registers the new envelope undercorewith a descriptive title.signed_success_payloadfield list mirrors the outer success arm minus protocol/version envelope fields andsigned_responseitself, on both single-target and bulk variants.#adcp-jws-profileanchor exists atdocs/building/by-layer/L1/security.mdx:589; the new normative paragraphs after L892 correctly defer alg allowlist, JWKS discovery, SSRF, and duplicate-key rejection to it.- Designated-task list (
verify_brand_claim,verify_brand_claims) matches the envelopetaskenum exactly. Closed-list invariant intact. - No new undiscriminated
oneOfintroduced.scripts/audit-oneof.mjs --checkwill not regress. - Pre-existing
result_entryin the bulk schema still usesstatuson per-result success while the single-target schema renamed toverification_status. Internally consistent, but the signed payload now locks the divergence in. Pre-existing, not introduced here — worth a follow-up to align the bulk per-result field name before 3.1 GA.
Follow-ups (non-blocking — file as issues before 3.1 GA)
- JWS Signing Input prose has variable-name shadowing.
security.mdx(new paragraph after L893) writesBASE64URL(UTF8(protected)) || "." || BASE64URL(UTF8(JCS(payload)))whereprotecteddenotes the decoded JSON header (the next clause says "protecteddecodes to..."), but the envelope schema also calls the wire fieldprotected— which holds the already-base64url-encoded value. A careful reader gets it right. A literal one double-encodes.ad-tech-protocol-expertflagged this Critical; I'm treating it as a Major follow-up rather than a block because the "decodes to" qualifier resolves the ambiguity, and RFC 7515 §5.1 is the conventional reading. Rename the formula variable (e.g.,BASE64URL(UTF8(protected_header))) before 3.1 GA so no implementer has to triangulate. - No verifier-side mechanism enforces per-
brand_domainJWK provenance. The spec MUSTs distinct response-signing JWK material andkidperbrand_domain, but a JWK in a shared multi-brand fleet's JWKS carries nobrand_domaintag, so a misconfigured signer using brand-A'skidto sign abrand_domain: brand-benvelope produces a signature any verifier accepts.security-reviewerMedium. Closest fix: add anadcp_brand_domainJWK member (or a per-brand_domainJWKS path) so verifiers can MUST-reject when the JWK's asserted brand does not byte-equal the payload'sbrand_domain. caller_identity: nullis replayable within the freshness window. Spec says "weaker evidence." That's prose, not a programmatic rule. Online verifiers consumingnot_oursas direction-asymmetric exclusion evidence should be required to refusecaller_identity: nullenvelopes as the sole basis for online trust decisions; audit verifiers can still accept them as historical evidence.- Clock-skew tolerance is unspecified. "Small clock-skew tolerance" interoperates badly. The shared profile uses ±60 s at
security.mdx:664. Pin the same value on online verification ofexp/iat. - Error arm is silently unsigned. The schema requires
signed_responseonly on the success arm. That's a defensible scope choice — rejections (disputed/not_ours) ride the success arm with averification_status, noterrors[]— but say so in the normative text. Otherwise a reader can't tell whether an unsigned error response is conformant or a downgrade attack. - Bulk extraction MUST belongs in
security.mdx, not onlyverify_brand_claims.mdx. The "audit stores extracting one result MUST also retain the original batch request and result index" rule is normative for any verifier reading bulk evidence; lift it into the designated-task section so audit-store implementers reading the L1 page see it. context_noteis now signed, durable, audit-retained free-form attacker-influenceable text. Existed pre-PR, but this PR turns it into stable LLM-injection surface for downstream classification pipelines. Add a non-normative implementer note mirroring thebrand_json_url"MUST be HTML-escaped" treatment atsecurity.mdx:1105, but framed for prompt-injection.- Credit the resolved issues. This PR resolves #4716 (replay protection), #4717 (per-brand JWK uniqueness), and #4718 (tenant binding) — all deferred to 3.2 by
.changeset/4703-response-signing-internal-consistency.md. AddCloses #4716 #4717 #4718to the PR body and prune the now-stale "3.2 hardening" disclaimer inverify_brand_claim.mdx(the diff already replaces that paragraph — confirm no other doc still references those issues as open).
Minor nits (non-blocking)
- Schema can't express
exp >= iat.response-payload-jws-envelope.json:46/:50lean on prose. Draft-07 can't, but a$commentwould help schema-only SDK readers. brand_domainpattern omits RFC 1035 length caps. Single-label hostnames (localhost) and >253-char strings validate. Permissive for a tenant identifier.agent_urlusesformat: "uri". Allows non-HTTPS and non-http(s)schemes. The narrative mandates HTTPS; the schema should reflect it.request_hash.descriptiondoes not list the canonical pre-image fields. SDK generators that consume only the schema can't reconstruct{task, brand_domain, agent_url, caller_identity, request}from prose elsewhere. Inline the binding object in the description.
Notable that this PR quietly pulls all three 3.2-deferred hardenings forward — the prior changeset's "3.2 hardening" disclaimer is now obsolete, which is good news the PR body undersells.
Safe to merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the core decoded-payload JWS envelope schema and requires signed_response on verify_brand_claim and verify_brand_claims success responses.
Documents the designated-task response-signing profile, request_hash binding, JCS/JWS signing input, per-brand response-signing key separation, freshness handling, and bulk audit retention.
Includes a changeset and was validated by the precommit suite, schema/docs checks, and git diff --check.