Repository navigation
Conversation
Advance live-status sequences only on recorded status-state changes so concurrently current assessments share a sequence during rolling deployments, rename the evidence path to /_ts/attestation.jws to match its application/jose body, clarify the discovery keys bound, and note that validators must select JSON Schema draft-07 explicitly.
How publisher attestation works end to end (high-level design)The demo shows what people see. This comment shows what happens behind it: every component, every request and every state change, from provisioning a key to an ad partner caching its result. It summarizes the three specs in this PR. Where a diagram or table says spec, it follows the specs' requirements. Where the specs leave a choice to the implementer, it says proposal. Status: this PR is design only. Nothing below is implemented or deployed.
Who is involved
1. System contextSplitting the verifier into these components, and the tooling-to-API transport, are a proposal. The specs fix the component responsibilities, not the service layout or the command names. The enrollment API is a provisional sketch (PA-ENROLLMENT-API). flowchart LR
subgraph OP["Publisher operator"]
TS["Operator tooling, ts CLI<br/>keys, config, enrollment"]
DNS[("Publisher DNS<br/>_ts-attestation TXT")]
end
subgraph EDGE["Publisher edge: released Trusted Server on Fastly or Axum"]
EP["/_ts/attestation.jws<br/>/_ts/attestation/keys.json"]
SNAP["Request-local settings snapshot<br/>[attestation] section"]
CFG[("trusted_server_config<br/>ts-publisher-key-NAME metadata")]
SEC[("trusted_server_secrets<br/>ts-publisher-NAME seed")]
EP --> SNAP
EP --> CFG
EP --> SEC
end
subgraph VER["Verifier / registry (component split is a proposal)"]
API["Enrollment and admin API<br/>authenticated"]
SCH["Scheduler<br/>renewals, hints, polling"]
COL["Collector<br/>60 s deadline, SSRF-guarded"]
POL["Policy evaluator<br/>canonical policy documents"]
DB[("Durable state<br/>enrollments, generation, sequence")]
CP[("Recovery checkpoint<br/>required for monitored or live-status")]
SIG["Isolated signer<br/>fencing epoch"]
PUB["Publication API<br/>read-only GET"]
API --> DB
SCH --> COL --> POL --> DB
DB --> CP --> SIG --> PUB
end
FA[("Fastly API<br/>read-only, P2")]
REL[("Layer 1 release evidence<br/>official WASM bytes, P2")]
CON["Consumer<br/>own trust configuration"]
MIR["Mirror or directory<br/>untrusted"]
TS -->|"provision seed and metadata, config push"| CFG
TS -->|"provision"| SEC
TS -->|"enrollment, key transitions, hints"| API
TS -->|"publish challenge"| DNS
COL -->|"TXT lookup, trusted resolver"| DNS
COL -->|"nonce, optional kid, HTTPS"| EP
COL -.->|"host mapping, active version, package identity"| FA
COL -.->|"authenticated release bytes"| REL
CON -->|"fresh nonce"| EP
CON -->|"assessments, optional status"| PUB
CON -.->|"same objects, verified the same way"| MIR
MIR -.->|"exact JWS bytes"| PUB
Trust never flows along the arrows from publisher or mirror to consumer. A consumer trusts a verifier only because its own configuration pins that verifier's key, policy IDs and policy digests. The publisher's 2. Publisher side: provisioning and configurationThe store names, entry prefixes and configuration fields are spec (PA-KEYS, PA-CONFIG). The tool flow and subcommands are a proposal; command names are not frozen. sequenceDiagram
autonumber
actor Op as Operator
participant TS as ts CLI
participant CS as trusted_server_config
participant SS as trusted_server_secrets
participant App as App config (ts config push)
participant Edge as Publisher edge
Op->>TS: Generate Ed25519 key locally, name it, set validity up to 90 days
TS->>CS: Check prefixes and existing refs for collisions, never overwrite unrelated entries
TS->>SS: Write seed to ts-publisher-NAME, standard base64, 32 bytes
TS->>CS: Write metadata to ts-publisher-key-NAME, schema ts-publisher-key-metadata-v1 with kid, jwk, not_before, not_after, secret_name
Note over TS: Private key bytes are never printed or sent to a verifier
Op->>App: Add [attestation] enabled, host, publisher_id, operator_id, deployment_id, environment, active_key, discovery_keys, verifiers, build_class
App->>Edge: ts config push publishes the settings envelope
Op->>Edge: Deploy a release that contains the endpoint
TS->>Edge: Self-check keys.json and attestation.jws
Key rules this enforces:
3. Publisher edge: what happens on each request (spec, PA-HTTP and PA-ROUTING)The attestation routes are dispatched before Edge Cookie creation, consent processing, device classification, bot challenges, request filters, publisher backend calls and auctions. They carry an explicit exception to generic authentication that applies only to these two read-only paths. flowchart TD
R["Request"] --> P{"Exact reserved path?"}
P -->|no| OR["Ordinary routing<br/>may reach publisher origin"]
P -->|yes| M{"Method is GET?<br/>HEAD and OPTIONS are not"}
M -->|no| E404a["404 not_found<br/>no origin fallback"]
M -->|yes| EN{"Attestation enabled<br/>for this deployment?"}
EN -->|no| E404b["404 not_found<br/>no store reads, no origin fallback"]
EN -->|yes| H{"Trusted ingress host equals<br/>configured [attestation].host?<br/>one attested host per deployment"}
H -->|no| E421["421 misdirected_request<br/>never borrows another host's settings"]
H -->|yes| Q{"Query and body valid?<br/>exactly one 32-byte nonce<br/>at most one canonical kid<br/>no other members, no body"}
Q -->|no| E400["400 invalid_request"]
Q -->|yes| RL{"Admission rate limit"}
RL -->|exceeded| E429["429 rate_limited"]
RL -->|ok| K{"Resolve key inventory"}
K -->|"no kid"| AK["Read active_key metadata only"]
K -->|"kid given"| DK["Read up to 4 discovery_keys metadata<br/>match thumbprint"]
AK -->|"unresolved or malformed"| E503c["503 unavailable"]
DK -->|"well-formed but unlisted"| E400k["400 unknown_kid"]
DK -->|"missing, malformed or ambiguous"| E503a["503 unavailable"]
DK -->|"one match"| SEL["Selected key"]
AK -->|"resolved"| SEL
SEL --> V{"Read only that seed<br/>derived public key equals metadata?<br/>key valid for the entire evidence lifetime?<br/>clock reliable?<br/>snapshot consistent?<br/>Fastly staging agrees with environment?"}
V -->|no| E503b["503 unavailable<br/>signs nothing"]
V -->|yes| B["Build binding from this request's snapshot<br/>plus adapter-observed platform identity"]
B --> D["projection digest = SHA-256 JCS of identity projection<br/>binding_id = SHA-256 JCS of binding"]
D --> S["Sign compact JWS<br/>alg Ed25519, typ ts-publisher-evidence+jws<br/>expires_at at most 60 s after issued_at"]
S --> OK["200 application/jose"]
Rules on every response from these paths, including errors:
Errors use a closed body, What goes into the signed evidence, all of it from the snapshot this request actually used:
4. Enrollment: proving control before any endorsementSpec (PA-ENROLLMENT):
Proposal: the request, verify and poll transport, and the exact step order. These follow the provisional PA-ENROLLMENT-API sketch, which is not a frozen v1 API. sequenceDiagram
autonumber
actor Op as Operator
participant API as Verifier enrollment API
participant DB as Verifier durable state
participant DNS as Publisher and operator DNS
participant COL as Collector
participant Edge as Publisher edge
Op->>API: Authenticated request with full tuple, public JWK and validity, policy, contact and recovery authorization
API->>DB: Check operator authorization record if operator_id is not null
API-->>Op: Single-use challenge, expires_at at most issued_at plus 600 s
Note over Op,API: Publisher TXT ts-publisher-v1=..., operator TXT ts-operator-v1=..., both base64url SHA-256 of JCS of the same challenge
Op->>DNS: Publish _ts-attestation.HOST TXT, and _ts-operator.OPERATOR-HOST TXT if a separate operator is named
Op->>API: Verify
API->>COL: Run every check before the challenge expires
COL->>DNS: Look up TXT via trusted resolver, from several vantage points if possible, record DNSSEC status
COL->>Edge: GET attestation.jws with fresh nonce over HTTPS, no redirects, pinned address
Edge-->>COL: Evidence JWS
COL->>COL: Strict Ed25519, schema, nonce, host, environment, every tuple value equal to the request
opt Policy requires provider corroboration, P2
COL->>COL: Corroborate provider host and service ownership and production version
end
alt All checks pass before the deadline
COL->>DB: Create enrollment, initialize authorization generation and status sequence, valid for at most 30 days
COL-->>Op: enrollment_id, first assessment follows
else Deadline passed or any check failed
COL-->>Op: Failed with reason codes, a new challenge is required
end
5. Collection and issuance: how an assessment is producedThe steps and invariants are spec (PA-COLLECTION, PA-TIME, PA-RECOVERY). Running scheduler, collector and signer as separate services is a proposal. sequenceDiagram
autonumber
participant SCH as Scheduler
participant COL as Collector
participant DB as Durable state
participant FA as Fastly API and release evidence, P2 only
participant Edge as Publisher edge
participant CP as Recovery checkpoint
participant SIG as Signer
participant PUB as Publication store
SCH->>COL: Start operation, 60 s total deadline
COL->>DB: Load enrolled tuple, authorized key, policy, current generation G
opt Policy needs provider observation
COL->>FA: Host mapping, active version, package identity
end
COL->>Edge: Fresh nonce, single-use with a 60 s life
Edge-->>COL: Evidence JWS
COL->>COL: Verify evidence, compare with enrolled tuple and provider view
opt Qualified selected-executable method only
COL->>FA: Verify retained official release bytes and eligibility, bind the selected executable
end
opt Policy needs provider observation
COL->>FA: Repeat observations, and a fresh evidence challenge if needed, to detect a change during collection
Note over COL: A race discards the run and allows at most one fresh attempt in a new bounded operation
end
COL->>COL: Evaluate checks and result under the canonical policy
COL->>COL: expires_at = min of observed_from + 900, key, enrollment, operator authorization, verifier signing key
COL->>DB: Recheck authorization, key, enrollment, deadlines and policy, then compare-and-swap against generation G
alt Generation changed, for example a withdrawal
DB-->>COL: Candidate discarded
else Generation still G
opt State changed and registry offers monitored or live-status
DB->>CP: Monotonic checkpoint update, acknowledged before anything affected is signed
end
CP-->>SIG: Signer holds the current fencing epoch
SIG->>SIG: Sign ts-publisher-assessment+jws within 60 s of observed_until
SIG->>PUB: Persist object and evidence, then publish
end
The fencing exists to stop one failure: a positive assessment published after a withdrawal. Two mechanisms cover it:
After a restart or restore, any of these conditions fences signing, positive commits and current-status publication with
Timing (PA-TIME):
What the assessment says. Five checks; any
The Fastly gate (qualification spec). Fastly's
Outcomes map to 6. What changes trigger, and the state each object moves throughThe specs define only these status values:
The state names below are an illustrative proposal that maps onto them. stateDiagram-v2
direction LR
state "Enrollment (spec status active or withdrawn)" as EN {
[*] --> ChallengeIssued: authenticated request
ChallengeIssued --> Failed: deadline or failed check
ChallengeIssued --> Active: DNS and key proofs pass
Active --> Active: renewal with fresh proofs within 30 days
Active --> ReviewHold: identity projection changed or DNS ownership in doubt, positive issuance stops
ReviewHold --> Active: reviewed and reauthorized
ReviewHold --> Withdrawn: review withdraws
Active --> Expired: 30 days without renewal
Active --> Withdrawn: exit, lost authorization, compromise, policy breach
Withdrawn --> [*]: terminal, recovery re-enrolls under a new ID
}
stateDiagram-v2
direction LR
state "Assessment (spec status current or withdrawn)" as AS {
[*] --> Current: signed and published
Current --> Superseded: new binding accepted, deploy or key transition
Superseded --> Expired: own expires_at, status still current until then, never renewed
Current --> Expired: own expires_at, a renewal is a separate new assessment
Current --> Withdrawn: confirmed problem
Superseded --> Withdrawn: confirmed problem
}
state "Publisher key (spec status active, retired or compromised)" as KY {
[*] --> Staged: publisher side only, in discovery_keys but not active_key
Staged --> Active: verifier records the key transition
Active --> Retired: its last assessment expired after rotation
Active --> Compromised: confirmed compromise
}
How each kind of change is handled (PA-TIME):
How quickly the verifier notices a deploy depends on the profile:
During that window the new version is unendorsed. Site traffic is unaffected. Key rotation without a key-only endorsement gap (PA-KEYS): sequenceDiagram
autonumber
actor Op as Operator
participant DNS as Publisher DNS
participant Edge as Publisher edge
participant V as Verifier
Op->>Edge: Stage successor seed and metadata with not_before no later than now, add it to discovery_keys, active_key unchanged
Op->>V: Request key transition for the successor
V-->>Op: Fresh DNS challenge naming the successor thumbprint
Op->>DNS: Publish the successor challenge TXT
V->>DNS: Verify the TXT
V->>Edge: GET attestation.jws with nonce and kid=successor
Edge-->>V: Evidence signed by the successor
V->>V: Record successor under a new generation, sequence advances
V->>V: Issue assessment for the successor binding, before activation
Op->>Edge: Switch active_key once metadata and config have propagated
Note over Edge,V: Fresh evidence already has a matching assessment, no atomic switch is promised
Op->>Edge: Keep the old key in discovery_keys until its last assessment expires
V->>V: Old key retired once its last assessment expires
7. Consumer side: how an ad partner decides (spec, PA-CONSUMER)flowchart TD
T["Own trust configuration<br/>pinned issuer keys, policy id, version, digest<br/>expected host and environment<br/>expiry-only or live status"] --> F["Fetch fresh evidence with a new nonce"]
F --> FV["Strictly verify evidence<br/>signature, schema, nonce echo, expiry"]
FV --> L["Get assessments from configured publication bases<br/>filter by binding_id from the evidence<br/>or reuse an unexpired retained copy"]
L --> S1["1 Bounded decoding, duplicate members, limits"]
S1 --> S2["2 Header, message type, trusted key for issuer"]
S2 --> S3["3 Strict Ed25519 verification"]
S3 --> S4["4 Schema and digest consistency"]
S4 --> S5["5 Issuer, policy and method allowlists"]
S5 --> S6["6 Time: skew, expiry, observation age"]
S6 --> S7["7 Exact equality with fresh evidence<br/>host, environment, binding"]
S7 --> S8["8 Result against the policy's required checks"]
S8 --> S9["9 Build assurance and release eligibility"]
S9 --> S10{"10 Live status required?"}
S10 -->|"no, default"| S11["11 Distinct-issuer threshold"]
S10 -->|yes| ST["Status matches issuer, enrollment, assessment, binding, key<br/>says current and active, at most 60 s old<br/>sequence not lower than highest seen"] --> S11
S11 --> RES["Local consumer result<br/>host, environment, accepted, scope<br/>reason_codes, evaluated_at, valid_until"]
8. Read surfaces (spec, PA-PUBLICATION)
Registry errors use the same closed body with Rules for mirrors and other transports, such as a registry REST API or MCP tool:
9. Withdrawal, compromise, exit and recoverysequenceDiagram
autonumber
actor Adm as Verifier incident admin
participant DB as Durable state
participant CP as Recovery checkpoint
participant SIG as Signer
participant PUB as Publication
actor Con as Live-status consumer
Adm->>DB: Withdraw enrollment or mark key compromised, with reason and evidence
DB->>DB: Advance generation and sequence atomically
DB->>CP: Acknowledge new state before anything affected is signed
CP-->>SIG: Withdrawn status may be signed
SIG->>PUB: Status with higher sequence, positive issuance stopped
Con->>PUB: Status for the cached assessment
PUB-->>Con: Withdrawn, cached result invalid
Note over Con: Expiry-only consumers that have not learned of it keep accepting until the delivered assessment expires
Note over DB,SIG: A restore from an older backup does not match the checkpoint and fences signing
10. Where each piece runs, by delivery phase
What the verifier must persist (proposal, derived from the spec's requirements):
11. What this does and doesn't prove
Open items:
|
aram356
left a comment
There was a problem hiding this comment.
Summary
This revision proposes the publisher attestation wire contract, the registry protocol and the Fastly qualification gate, with a draft-07 schema and signed vectors. I independently verified the vectors' signatures, JWK thumbprints, JCS bytes and digests (OpenSSL, ed25519-dalek verify and verify_strict, plus a separate ajv run), and they are internally consistent. The blocking issues are:
- schema patterns that admit IP literals;
- a consumer algorithm that never checks the nonce;
- two statements that don't match the codebase: a host-based configuration selector, and production Axum support;
- no vectors for the cases the default Rust libraries accept;
- two open contract questions;
- the PR's state.
7 of the inline comments below carry a one-click GitHub
suggestion. Use Commit suggestion, or Add suggestion to batch to apply several at once, to commit them to the PR branch. Each was checked alone and together against the fixtures, ajv andprettier --checkindocs/. The other comments describe their fix in prose, because the change touches several places or needs regenerated signatures.
Blocking
🔧 wrench
httpsIdentifieraccepts IP literals: see inline atpublisher-attestation-v1.schema.json:98hostadmits names that URL parsers turn into IPv4 addresses: see inline atpublisher-attestation-v1.schema.json:91- The fingerprint context allows a Fastly binding with unknown staging: see inline at
publisher-attestation-v1.schema.json:966 - The consumer algorithm never requires the nonce to match: see inline at
2026-11-04-publisher-attestation-registry-design.md:195 - There is no existing trusted configuration selector: see inline at
2026-11-04-publisher-attestation-design.md:263 - Production Axum attestation contradicts the adapter's documented support status: see inline at
2026-11-04-publisher-attestation-design.md:62 - The vectors miss the two rejections the default Rust stack gets wrong: see inline at
publisher-attestation-v1-vectors.json:1243 - PR state and description disagree about merging: see the cross-cutting findings below
browser integration testswas cancelled: see the cross-cutting findings below
❓ question
- Which component emits the 429?: see inline at
2026-11-04-publisher-attestation-design.md:247 - Where does a consumer get "the policy's required checks"?: see inline at
2026-11-04-publisher-attestation-registry-design.md:77
Non-blocking
♻️ refactor
- Build on the
/_ts/traceterminal dispatcher instead of a new route shape: see inline at2026-11-04-publisher-attestation-design.md:249 - Commit a reproducible generator with a public test-only seed: see inline at
publisher-attestation-v1-vectors.json:2 - Reader load: see the cross-cutting findings below
🤔 thinking
- Size the startup seam against Fastly's per-request secret budget: see inline at
2026-11-04-publisher-attestation-design.md:271 - Explain why seeds don't follow the request-signing store precedent: see inline at
2026-11-04-publisher-attestation-design.md:307 build_reportis outside the binding, so assessments don't cover it: see inline at2026-11-04-publisher-attestation-design.md:204- Make staging/environment agreement a binding invariant, not only an endpoint rule: see inline at
2026-11-04-publisher-attestation-design.md:188 - "Clock unavailable" and key-boundary behavior need testable definitions: see inline at
2026-11-04-publisher-attestation-design.md:245 - The
operator_idpath carries no authority: see inline at2026-11-04-publisher-attestation-design.md:267 - How does
[attestation].hostrelate topublisher.domain?: see inline at2026-11-04-publisher-attestation-design.md:279 - Consider splitting P1 so the edge work isn't gated on verifier operations: see inline at
2026-11-04-publisher-attestation-design.md:468 - A reference verifier inside the publisher's own CLI isn't independent: see inline at
2026-11-04-publisher-attestation-registry-design.md:223 - The vectors need a defined harness contract: see inline at
publisher-attestation-v1-vectors.json:36
⛏ nitpick
- The vectors' identifiers contradict the protocol rules they test: see inline at
publisher-attestation-v1-vectors.json:860 - RFC 8032 already requires the
Sand canonical-encoding checks: see inline at2026-11-04-publisher-attestation-design.md:157 - The API documents
metadata.hashsumas deprecated, not obsolete: see inline at2026-11-04-publisher-attestation-fastly-qualification-design.md:34
🏕 camp site
- Mark the January draft superseded: see the cross-cutting findings below
Cross-cutting / body-level findings
- 🔧 PR state and description disagree about merging. The PR was marked ready for review on 2026-11-10, but the description still says "Keep this PR in draft for spec approval" and "This draft must not be merged as a spec-only closure of independent deployment verification", while also saying
Closes #161. If a maintainer approves the spec and merges, GitHub closes #161 with no implementation. Fix one of two ways:- convert the PR back to draft until the implementation lands; or
- change
Closes #161toPart of #161, and update the description to say what approving this revision means.
- 🔧
browser integration testswas cancelled. Run 42188253781 cancelled the job after 16 minutes with no failed step, which looks like a timeout. It isn't a required check and this diff is docs-only, so a re-run should clear it. - ♻️ Reader load. The contracts are hard to review, and will be hard to implement from:
- Several normative sections are single paragraphs carrying many separate requirements: PA-CONFIG (design line 291, about 450 words mixing config schema, discovery reads,
kidselection and failure modes), PA-WIRE (design line 161), PA-ENROLLMENT (registry lines 108 and 116) and PA-STATUS (registry line 96). Numbered requirement lists or tables per PA group would let reviews and tests cite individual rules. - The review logs (design lines 544-558, registry lines 326-335, qualification lines 138-145) record process rather than contract, and repeat "no blocking findings". The commit history and this PR thread already hold that.
- "The fork" (design lines 155 and 455) and "Layer 1" are used before they're defined, and section 14 identifies the fork only indirectly.
- The end-to-end explainer comment on this PR is a good primer. A condensed version as the spec's overview would serve readers better than the current "Read this first".
- Several normative sections are single paragraphs carrying many separate requirements: PA-CONFIG (design line 291, about 450 words mixing config schema, discovery reads,
- 🏕 Mark the January draft superseded.
docs/superpowers/specs/2026-01-15-attestation-design.mdstill says "Status: Proposal", and #161's description links to it as the starting point. This spec mentions it only as "the historical January proposal" (design line 459), without a link. Add a superseded note to the January draft that points here, and link to it from line 459, so readers don't follow the old full-config and embedded-checksum design.
CI Status
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- Analyze (python): PASS
- Analyze (rust): PASS
- CI Node pin guard: PASS
- CLAUDE.md symlink guard: PASS
- CodeQL: PASS
- HTML EOF runtime (Fastly): PASS
- browser integration tests: CANCELLED
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- docs-proposal scripts: PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- trace runtime and browser acceptance: PASS
- ts dev lint domains (changed lines): PASS
- vitest: PASS
- worker-deadlines (cloudflare): PASS
- worker-deadlines (spin): PASS
Correct host and environment validation, challenge verification, policy evaluation and runtime support boundaries. Add deterministic signed fixtures and bounded regression checks, keeping provider qualification and implementation as explicit future gates.
Place the publisher supersession note outside the status metadata so it combines with the release-history correction already on main. Preserve the current publisher contract and historical provenance.
|
Published the review corrections in 1bb9da9. 226f7d3 places the January supersession note separately so it coexists with the release-history correction on main. The PR is back in draft, preserving the existing implementation-before-merge boundary for #161. Its description now reflects development-only Axum and the P1a/P1b split. PA-WIRE, PA-CONFIG, PA-ENROLLMENT and PA-STATUS use numbered requirements. Historical review logs remain because CONTRIBUTING requires them; their earlier clean results are not current approval. The January proposal links to the current publisher contract, which links back. Local required checks passed, including the pinned adapter/runtime gates, documentation checks, JS suite, byte-identical fixture regeneration and 110 fixture checks covering 45 synthetic cases. A separate Rust cryptography probe checked interoperability and the library-default pitfalls. Runtime attestation implementation and live provider qualification remain future gates. On 226f7d3, all four required checks pass and 28 checks are successful overall. All 24 inline conversations have confirmed replies and are resolved. The two non-required browser jobs are still running, including the fresh replacement for the previously cancelled browser integration attempt; they are not claimed passed. Both new commits are GitHub Verified. The PR remains draft pending human spec approval and later implementation/qualification. |
aram356
left a comment
There was a problem hiding this comment.
Summary
Round 2 at 3b61ae1. Of the 28 round-1 findings, 22 are fixed, 5 are partly fixed and 1 is not fixed:
- Partly fixed: B6 and B7 (see R6 and R5), B11 (the CI re-run), and N16 (reader load, now mostly addressed). For N16 I'm withdrawing the review-log point, because CONTRIBUTING.md:99 requires a
## Review logsection. - Not fixed: B10, the PR state.
I independently re-verified the regenerated fixtures: signatures, thumbprints, JCS bytes, digests and schema results all pass. The generator's --check and all 110 tests also pass, with byte-identical output across Node versions and locales.
New blocking issues:
- The identity model has no notion of a publisher that owns several sites, and
publisher_idis an unverified label (R1, R2). - The new harness doesn't perform the stage-10 status checks or the trust-key selection the registry requires, and no vector covers them (R3, R4).
- Two new negative vectors don't isolate what they claim (R5, R6).
3 of the inline comments below carry a one-click GitHub
suggestion. Use Commit suggestion, or Add suggestion to batch to apply several at once. Each was checked alone and together, run the way CI runs them:generate.mjs --check,node --test scripts/publisher-attestation/*.test.mjs(110 pass), prettier ondocs/and on the scripts, and no other changes to the tree. The rest describe their fix in prose, because each one touches the generator, the fixture or several sections.
Blocking
🔧 wrench
- There is no publisher-to-site model, and
publisher_idis an unverified label: see inline at2026-11-04-publisher-attestation-design.md:107 - The harness never checks status contents, and no vector does: see inline at
harness.test.mjs:287 - The verifier key comes from the fixture, not from the consumer's trust configuration: see inline at
harness.test.mjs:116 non_canonical_rcan't catch a missing R canonicality check: see inline atgenerate.mjs:211non_canonical_operator_identifiernow fails for two reasons: see inline atpublisher-attestation-v1-vectors.json:2205- PR state and
Closes #161still disagree: see the cross-cutting findings below - Two browser jobs failed on runner DNS: see the cross-cutting findings below
❓ question
- Can a publisher reuse one key across its own sites?: see inline at
2026-11-04-publisher-attestation-registry-design.md:146
Non-blocking
🤔 thinking
- The attested host is the site; don't anchor it to
publisher.domain: see inline at2026-11-04-publisher-attestation-design.md:339 - Renewal cost scales with sites, not just operators: see inline at
2026-11-04-publisher-attestation-registry-design.md:230 - The harness's stage order differs from the registry's ordered stages: see inline at
harness.test.mjs:254 - An honest
not_satisfiedresult is labelledmalformed: see inline atharness.test.mjs:275 - Verifier issuance doesn't match the operator against its authorization record: see inline at
harness.test.mjs:177 - The normative built-in policy documents live in a test-fixture file: see inline at
2026-11-04-publisher-attestation-registry-design.md:90
♻️ refactor
- Make every declared harness input take effect, or drop it: see inline at
harness.test.mjs:103
🌱 seedling
- Publisher-level read surfaces and multi-site vectors: see inline at
2026-11-04-publisher-attestation-registry-design.md:375
⛏ nitpick
- The
policyCheckcomment contradicts the registry's method semantics: see inline atpublisher-attestation-v1.schema.json:1436 - Dead delete of a field that never existed: see inline at
generate.mjs:279 - Most assertions have no message: see inline at
vectors.test.mjs:38 - The new fixture gate isn't documented: see the cross-cutting findings below
- Smaller tooling and spec items: see the cross-cutting findings below
Cross-cutting / body-level findings
- 🔧 PR state and
Closes #161still disagree. The PR was converted to draft at 14:57Z on 2026-11-11 and marked ready for review again at 15:49Z. GitHub still lists #161 under closing issues. Meanwhile the description says "This PR remains draft for specification approval" and "Closes #161 only when…", and the 15:22Z summary comment says "The PR is back in draft". GitHub ignores the qualifier, so merging this revision closes #161. Fix one of two ways:- convert the PR back to draft; or
- change
Closes #161toPart of #161, and update the description and summary to match the actual state.
- 🔧 Two browser jobs failed on runner DNS. In run 43052263577,
browser integration testsandtrace runtime and browser acceptancefailed atInstall Playwright/Install browser dependencieswith "Temporary failure resolving 'azure.archive.ubuntu.com' … Failed to install browsers". In the browser job, that skippedBuild browser config generator, and the test steps then failed withgenerate-viceroy-config ENOENT. This is a runner infrastructure problem, not a PR defect; please re-run both jobs. - ⛏ The new fixture gate isn't documented. The
format-docsstep that runsgenerate.mjs --checkand the node tests is missing from three places:- AGENTS.md "CI Gates", where it would be item 14, noting that
cd docs && npm ciis required forajv; .claude/commands/check-ci.md;- the
scripts/README.mdtable.
- AGENTS.md "CI Gates", where it would be item 14, noting that
- ⛏ Smaller tooling and spec items.
- Parser depth bound.
harness.test.mjs:56usesdepth > 16, which admits one more nesting level than PA-WIRE's "bounded depth 16". - Size cap. Line 95 applies a single 32,768-byte cap to every type; the spec says evidence 16 KiB and status 8 KiB. Trailing data throws a
SyntaxErrorinstead of rejecting at stage 1. - Point length.
point-admission.mjs:32strictPointhas no 32-byte length check: 33- and 35-byte inputs holding a valid key are accepted, and empty input throws. - Duplicate-member tests.
vectors.test.mjs:136-139joins the two patterns with||, so either duplicate case passes on the other's pattern. - Coverage held only by
--check. A flipped byte in the S half of a signature segment, or inenrollment_txt_value, leaves the node tests green; onlygenerate.mjs --checkcatches it. Reordered keys pass--checkbecause output keeps input order. Either document that or sort the output. - README.
README.md:17-18says to run prettier after generating, butgenerate.mjs:290-297already does. - CI step order. The fixture step runs after the VitePress build, so a dead-link failure hides fixture results. No lint or prettier check covers
scripts/publisher-attestation/*.mjs. - Spec wording.
- The
error_exampleexpectation says "unlisted or invalid kid", but design line 327 maps a malformed kid toinvalid_request. - Design line 347 assigns the read counts to T5/T7, but the T7 row omits them.
- The PA-FAILURE table still defines
clock_unavailableas "Reliable current time unavailable", contradicting the new definition at line 415. - Line 339 calls
publisher.domaina "cookie domain", butpublisher.cookie_domainis a separate setting (settings.rs:53), and no T-row tests the new host rule.
- The
- dalek pitfall.
VerifyingKey::to_bytes()returns the input bytes, so an implementer who "re-encodes" through it passes a non-canonical key. PA-WIRE item 13 could nameto_edwards().compress(). - Reader load. PA-WIRE's numbering splits sentences into dependent fragments (items 20 and 37), the exclusive-expiry rule (line 180) sits unnumbered between items 13 and 14, and PA-HTTP (line 325) and lines 345-349 remain dense.
- Parser depth bound.
CI Status
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- Analyze (python): PASS
- Analyze (rust): PASS
- browser integration tests: FAIL (runner DNS failure installing browsers; re-run)
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- CI Node pin guard: PASS
- CLAUDE.md symlink guard: PASS
- CodeQL: PASS
- docs-proposal scripts: PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- HTML EOF runtime (Fastly): PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- release scripts: PASS
- trace runtime and browser acceptance: FAIL (runner DNS failure installing browsers; re-run)
- ts dev lint domains (changed lines): PASS
- vitest: PASS
- worker-deadlines (cloudflare): PASS
- worker-deadlines (spin): PASS
|
Round-2 corrections are published in 9a3a9ee, with diagnostic follow-up 1db75cd. Both commits are GitHub Verified. Publisher owner identity is now authorized against a verifier-held account/site record, with explicit per-site key and renewal costs. The harness enforces pinned verifier trust, signed status target/state checks and nonce/time precedence. The negative vectors isolate their stated cases, normative policies are separate files, and fixture lint/format/gate instructions are documented. The follow-up corrects literal assertion-message placeholders and adds the lint guard. The full 44-check local batch passed on 9a3a9ee, including pinned runtime fixtures and 3,299 JS tests. The diagnostic-only follow-up reran all 169 fixture tests, regeneration, lint, formatting and domain lint; test-module AST comparison establishes that its only test changes are assertion messages. Other runtime/build verification inputs are unchanged. All four required hosted checks now pass on final head 1db75cd. The 17 reviewer conversations have confirmed replies and are resolved; the five new bot conversations have replies and were automatically resolved by GitHub. A fresh read confirms zero open inline conversations. Additional non-required browser/integration jobs and Rust analysis are still running, so they are not claimed passed. The fresh browser jobs are new attempts after the earlier runner-DNS failures. The PR remains draft, with |
Make the synthetic fixture runner enforce the stage-4 rules the registry claims the vectors cover: per-check digest equality, assessment observation intervals, the 900-second cap, 60-second evidence/status lifetimes and the evidence key validity window. Replace the envelope bound test with inputs that reach the size, UTF-8 and base64url checks. Give expired-window and retired publisher keys one reason code (key_unauthorized), require a provider hash for fingerprint_only, retitle consumer stage 10, define cross-object stage order, report verifier issuance decode failures as its stage 1, require non-null Fastly service identity in prose and cap issuer metadata. Add 13 negative cases.
Summary
Define publisher declarations and portable independent assessments, so consumers can choose which verifiers and policies to trust and distinguish a fresh declaration from qualified deployment evidence.
This PR is for specification approval. It contains proposed contracts, structural schemas, synthetic signed fixtures and fixture tooling. Production implementation requires maintainer spec approval and an implementation plan. Do not merge this revision as closure of independent deployment verification.
Contract
V1 defaults to assessment expiry within 15 minutes of the oldest observation. Live status is optional. Identity projection consistency remains publisher-declared; later fresh build reports are not covered by an earlier assessment. Ordinary deployment changes can leave a temporary endorsement gap.
P1a covers endpoint, runtime keys, pure verification and fixtures. P1b covers the separate reference verifier and declaration enrollment after task-force decisions on verifier operation/trust. Provider qualification and operated registry recovery remain separate gates. Auction/bid attestation and a request-signing bridge are outside this PR.
Review corrections
publisher_unauthorized.observed_from <= observed_until <= issued_at, issuance within 60 seconds, expiry within 900 seconds ofobserved_fromindependent of policy), 60-second evidence/status lifetimes and the evidence key validity window.key_unauthorized. An inactive pinned verifier key remains stage 2signature_invalid.provider_files_hashforfingerprint_onlyassurance in prose and schema.Validation
The full 44-check local batch passed on 9a3a9ee with Rust 1.98.1, Node 24.20.0, Viceroy 0.21.0, Spin 4.1.0, wasm-bindgen 0.2.126 and CI-matched Wrangler 4.83.0. Native test/dev debug information was disabled to bound local disk use.
Follow-up 43d62dc changes only specifications, schema, fixtures and fixture tooling (no Rust, JS runtime or workflow inputs). On it: byte-identical fixture regeneration, 185 fixture tests (172 declared cases), fixture ESLint and Prettier, documentation format/lint/VitePress build and changed-lines domain lint passed under Node 24.12.0. All 13 new cases fail against the previous harness; removing each new check individually fails exactly its own case, and reverting the schema rule fails its schema test. Earlier hosted results do not certify 43d62dc.
Synthetic fixtures are not a production verifier, real DNS enrollment, live provider qualification, or a multi-issuer aggregation test.
Completion gates
Tracking
Part of #161. This specification revision does not close that implementation issue; add closure only with the completed implementation.
Related: #1427. Binary-attestation dependencies: #1235, #1435 and #1449.
Closes #161