Skip to content

Add publisher attestation - #1494

Open
prk-Jr wants to merge 13 commits into
mainfrom
feature/publisher-attestation
Open

prk-Jr wants to merge 13 commits into
mainfrom
feature/publisher-attestation

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Publisher endpoint and common wire rules: exact host/deployment/key bindings, strict JWS, consumer-generated nonce checks, runtime store/snapshot integration and privacy. Fastly is the production target; Axum remains development-only.
  • Registry protocol and operations: independent enrollment, pinned complete policy documents, assessments, withdrawal, recovery and consumer acceptance. The operator REST API remains provisional.
  • Fastly qualification gate: required evidence before an official installed-executable claim. A package fingerprint alone cannot qualify that claim.

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

  • Separate publisher owner identity from site hosts. Use an owner-domain publisher identifier with verifier-held account/site authorization, finite proof and withdrawal; reject foreign-account claims with publisher_unauthorized.
  • Retain distinct per-site keys in v1 and document secret/rotation costs, monthly DNS renewal burden, exact-host alias behavior and named multi-site/delegation follow-ups.
  • Make the synthetic harness enforce independent verifier JWK/kid/validity, signed status target/state checks, fresh-evidence nonce/time precedence and exact finite publisher/operator authority.
  • Repair noncanonical R using a signature accepted by relaxed point comparison, explicitly noting its unavoidable small-order overlap. Isolate the malformed operator-identifier case.
  • Extract normative policy documents; define honest unsuccessful-policy rejection; remove inert inputs and document the one-issuer/structured-scope limits.
  • Enforce exact point length and parser/type-size bounds, strengthen signature/TXT/duplicate-member assertions and diagnostic messages, and document/lint/format fixture tooling before the documentation build.
  • Make stage 4 enforce the rules the registry says the vectors cover: per-check digest equality, assessment observation intervals (observed_from <= observed_until <= issued_at, issuance within 60 seconds, expiry within 900 seconds of observed_from independent of policy), 60-second evidence/status lifetimes and the evidence key validity window.
  • Give one reason code to publisher keys that do not cover the evidence lifetime (stage 4) or that status reports retired (stage 10): key_unauthorized. An inactive pinned verifier key remains stage 2 signature_invalid.
  • Require a non-null provider_files_hash for fingerprint_only assurance in prose and schema.
  • Retitle consumer stage 10 to cover known withdrawal/compromise in every mode before optional live status. Define cross-object order: each stage applies to fresh evidence before the candidate assessment. Report verifier-issuance decode/signature failures as its own stage 1.
  • Require non-null Fastly service identity in every Fastly binding (503 when unobservable), cap issuer metadata at 16 KiB, and scope the 524,288-byte list bound to assessment-list and key-history responses.
  • Replace the envelope-bound test with inputs that reach the per-type size, UTF-8 and canonical base64url checks.

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.

  • Rust formatting, all eight Clippy aliases, all five adapter test aliases, 29 parity tests, ten native build-digest tests and lint, and WASM clock lint passed.
  • JS build, 3,299 Vitest tests with no type errors, JS formatting, documentation lint/format/build and Markdown formatting outside docs passed.
  • Documentation-proposal checks, CI Node-pin tests/guard, changed-lines domain lint and release-producer script tests/shellcheck passed.
  • Fastly deadline fixture: five passing tests. Cloudflare and Spin fixture build/lint/runtime: four and five passing cases respectively.

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

  • Maintainer spec approval before implementation planning.
  • Freeze provisional publisher-authorization/operator workflow encoding before P1b implementation.
  • Recheck shipped Layer 1 dependencies and prepare the implementation plan.
  • Implement the runtime and adapter regressions.
  • Complete required provider/operational qualification before strong claims or merge.

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

@prk-Jr prk-Jr self-assigned this Nov 4, 2026
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.
@prk-Jr

prk-Jr commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

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.

  • P1: the edge endpoint, the reference verifier and the pure verification library.
  • P2: provider-backed and official checks, gated on the Fastly qualification.
  • P3: the operated registry backend: renewal and status service, durable state, recovery checkpoint and signer fencing.

Who is involved

Party What it runs What it holds
Publisher edge The released Trusted Server binary (Fastly or Axum) with two fixed routes Its own Ed25519 seed in the existing secret store, public key metadata in the existing config store
Publisher operator Operator tooling (ts CLI) for keys, config and enrollment, plus DNS DNS write access and provider deploy rights, which are never shared with a verifier
Verifier (registry) Enrollment, collection, policy evaluation, signing, read-only publication Its own signing key, enrollment state and sequence counters. In P2, read-only provider credentials
Consumer (SSP, DSP) Its own verification library and trust configuration Pinned verifier keys, allowed policies, cached results
Mirror or directory Optional. Aggregates read surfaces from several verifiers Nothing trusted. Serves exact signed bytes

1. System context

Splitting 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
Loading

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 verifiers list and the verifier's metadata document are hints.


2. Publisher side: provisioning and configuration

The 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
Loading

Key rules this enforces:

  • Disabled by default: with attestation off, the edge reads no attestation stores and adds no resources.
  • Separate production and staging keys: they use different deployment IDs and keys. On Fastly, isolation comes from version-specific physical store links or a separate service. Activating the same version on both networks shares its key and is not isolation.
  • Official claims need the release binary: the endpoint must be compiled into the authenticated release executable. A custom build cannot inherit official release identity.

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"]
Loading

Rules on every response from these paths, including errors:

  • Cache-Control: no-store, private, Surrogate-Control: no-store and X-Content-Type-Options: nosniff.
  • No ETag, Last-Modified, Set-Cookie or redirect, and never a 304.
  • Wildcard CORS without credentials.

Errors use a closed body, {"schema":"ts-publisher-error-v1","error":<code>}, of at most 256 bytes, which never names the failing setting, key or store.

What goes into the signed evidence, all of it from the snapshot this request actually used:

Part Source
binding Identity fields from settings: host, publisher_id, operator_id, deployment_id, environment. platform is adapter-observed: Fastly service, version and staging flag, or nulls on Axum. Also the key's public JWK and validity, and the identity projection with its digest
binding_id SHA-256 of the JCS binding. It changes with a new service version, key or covered identity value, and not when an assessment renews
nonce Echoed from the caller. The edge keeps no nonce cache. The caller makes each nonce single-use
build_report Operator declaration (custom or unspecified) plus available build facts. A declaration, not a measurement

keys.json goes through the same path, method, enabled, host and rate-limit checks. Any query is a 400. It reads metadata for every listed key name, without enumerating the store. It returns the host, 1 to 4 keys sorted by thumbprint and the untrusted verifiers list. If any listed entry is missing or inconsistent it returns 503 instead of a partial set.


4. Enrollment: proving control before any endorsement

Spec (PA-ENROLLMENT):

  • the challenge contents;
  • the 10-minute deadline;
  • the DNS and key-possession proofs;
  • the operator authorization requirement;
  • the 30-day limit.

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
Loading
  • Both proofs are required: DNS proves control of the exact host, and the signed evidence proves possession of the exact key. A parent domain doesn't cover subdomains, and wildcards are never accepted.
  • Renewal repeats the proofs: within 30 days the publisher re-proves DNS and key possession. Tooling should automate it, and the registry never receives DNS write credentials.
  • The fetch is guarded against SSRF: loopback, private and metadata addresses are rejected. Every connection candidate is resolved, validated and pinned. Each call has a 5-second deadline and size caps (16 KiB for evidence).

5. Collection and issuance: how an assessment is produced

The 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
Loading

The fencing exists to stop one failure: a positive assessment published after a withdrawal. Two mechanisms cover it:

  • Generation compare-and-swap: required in every registry mode, including expiry-only. A worker that started before the withdrawal sees the generation changed, and its candidate is discarded.
  • External checkpoint: a production registry offering the monitored or live-status contract also needs one. It sits outside the rollback domain of ordinary backups and is acknowledged before any object affected by a state change is signed. The signer must hold the current fencing epoch.

After a restart or restore, any of these conditions fences signing, positive commits and current-status publication with state_recovery_required:

  • a lower sequence or generation;
  • an inconsistent state digest;
  • an unknown checkpoint;
  • a stale epoch;
  • a checkpoint outage.

Timing (PA-TIME):

  • Ordering: observed_from ≤ observed_until ≤ issued_at < expires_at ≤ observed_from + 900, and issuance happens within 60 s of observed_until.
  • Expiry cap: an assessment never outlives the publisher key, the enrollment, the operator authorization or the verifier signing key.
  • Renewal: every 5 minutes by default. Each renewal is a new assessment; nothing extends an existing one.
  • Evidence: lives at most 60 s.
  • Status: lives at most 60 s, also capped by those authorizations.

What the assessment says. Five checks; any fail or unavailable means the result is not satisfied:

Check publisher-declaration-v1 requirement
host_authorization pass via dns-key-control-v1
key_possession pass via publisher-evidence-jws-v1
configuration pass via public-identity-projection-v1
host_service_binding not_assessed, or pass with an allowlisted method
active_version not_assessed, or pass with an allowlisted method

build.assurance is one of:

  • declared: coverage declaration_only.
  • fingerprint_only: coverage provider_package_fingerprint. A non-null provider files hash requires host_service_binding and active_version to pass.
  • artifact_bound: coverage selected_executable_only. publisher-declaration-v1 never accepts it. Among built-in policies only fastly-official-deployment-v1 requires it, and no verifier issues that policy until the Fastly qualification passes. provenance: official additionally needs a non-null executable digest and a verified eligible release.

The Fastly gate (qualification spec). Fastly's files_hash is SHA-512 over concatenated package file contents, without names or lengths. A matching hash therefore doesn't prove which module runs. A strong method must be one of these, and it needs two independent reviews before it is enabled:

  • a provider-authenticated selected-module digest;
  • authenticated selected-byte readback;
  • a qualified package-hash binding.

Outcomes map to artifact_bound, fingerprint_only, declared or unavailable. Any CLI, provider or package-layout change requires requalification.


6. What changes trigger, and the state each object moves through

The specs define only these status values:

  • enrollment: active or withdrawn;
  • assessment: current or withdrawn;
  • publisher key: active, retired or compromised.

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
  }
Loading
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
  }
Loading

How each kind of change is handled (PA-TIME):

Change Verifier action Status sequence
Renewal of the same binding New assessment every 5 minutes by default Unchanged
New service version (deploy) Reassess the new binding. The old assessment stays current until expiry and is not renewed Advances
Key transition Prove the successor via ?kid=, record it, assess it before activation Advances
Changed identity projection (deployment ID, environment, …) Stop positive issuance pending enrollment review. Existing assessments lapse at expiry unless authorization was lost Advances when the review withdraws or reauthorizes
Lost host mapping or authorization, provider contradiction, key compromise Withdraw affected assessments Advances

How quickly the verifier notices a deploy depends on the profile:

  • Monitored Fastly profile: up to 60 seconds of polling, plus collection.
  • Declaration profile: up to the 5-minute renewal, unless the operator sends a reassessment hint.

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
Loading
  • The whole binding must match: pre-assessing avoids a gap only if the successor's assessment covers the whole live binding, meaning the same service version as well as the new key. If a deploy lands in between, the successor must be reassessed first.
  • The old key stays verifiable: for at least the maximum assessment lifetime plus the status and cache allowance.

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"]
Loading
  • First failure decides: the first failing stage gives the reason code, for example binding_mismatch at stage 7, artifact_binding_unproven at stage 9, or withdrawn, key_compromised or status_unavailable at stage 10.

  • valid_until: it is the earliest of:

    • each supporting assessment's expiry;
    • local trust-key limits;
    • any observation-age limit.

    In live mode it also includes each required status's expires_at and issued_at plus the configured status maximum age. A shorter configured cache lifetime counts from evaluated_at, and reading the cache never resets it.

  • Cache invalidation, in any mode: a cached result is never used at or after valid_until. Newly known withdrawal or compromise, or any change to the consumer's trust, policy, threshold or status settings, invalidates it earlier. In live mode, learning a higher status sequence also invalidates it.

  • Expiry-only is the default: a retained valid assessment needs no registry call per check and stays usable through a registry outage until it expires. Initial discovery and renewal still need the registry. A withdrawal the consumer hasn't learned about stays unseen until expiry, at most 15 minutes after the oldest observation. A withdrawal it does know about is always honored.

  • Host-level only: a consumer checks a host when it starts trading with it and re-checks on its own schedule. Attestation is not a per-bid lookup. A bid request's domain fields are claims and aren't cryptographically linked to this result in v1.


8. Read surfaces (spec, PA-PUBLICATION)

Surface Path Response Caching
Publisher evidence GET https://HOST/_ts/attestation.jws?nonce=…[&kid=…] Evidence JWS or closed error body no-store, private
Publisher keys GET https://HOST/_ts/attestation/keys.json 1 to 4 public keys, untrusted verifiers hints no-store, private
Issuer metadata GET {issuer}/v1/issuer Bases, keys, policies, live-status support. Untrusted until pinned Not specified
Host lookup GET {base}/v1/hosts/HOST/assessments?environment=… Newest unexpired per issuer, binding and policy. Withdrawn entries suppressed. Optional binding_id, policy, issuer filters. Max 16 items and 524,288 bytes, else 409 no-store
Assessment GET {base}/v1/assessments/ID Exact JWS bytes May use public, immutable, max-age no greater than its remaining life
Status GET {base}/v1/assessments/ID/status Status JWS. 404 once the assessment expired, 503 if unavailable no-store
Key history GET {base}/v1/hosts/HOST/key-history?environment=… One entry per issuer, policy, key and binding. Retained 90 days by default no-store

Registry errors use the same closed body with invalid_request (400), not_found (404), conflict (409) and unavailable (503).

Rules for mirrors and other transports, such as a registry REST API or MCP tool:

  • They must return the exact JWS bytes.
  • Listing marks must show policy, assurance, issuer and expiry, not a bare check mark.
  • A host stops showing as assessed once its newest assessment has expired or been withdrawn.

9. Withdrawal, compromise, exit and recovery

sequenceDiagram
  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
Loading
  • Publisher exit:

    1. The verifier checks an enrolled credential, or approved recovery or fresh DNS proof.
    2. It records the request, stops renewal immediately and withdraws.
    3. The publisher gets an explanation and a review and correction route.

    An appeal can't extend expired objects or turn a failure into success.

  • Publisher key compromise: affects all assessments for that key in that enrollment. Known reuse across enrollments triggers investigation and withdrawal of each one. Recovery provisions a fresh key, re-proves host control and re-enrolls under a new enrollment ID. An old key can never authorize its own replacement.

  • Verifier key compromise: handled by consumers withdrawing trust and reissuing through trusted recovery. A status signed only by the compromised key can't fix it.

  • state_recovery_required: an authorized recovery either restores the latest corroborated state, or withdraws the affected enrollment and re-enrolls under a new identifier.

  • Withdrawal is local: it affects only that verifier's endorsement. It isn't a global kill switch, and it never touches publisher traffic.


10. Where each piece runs, by delivery phase

Phase Edge Verifier Consumer
P0 provider feasibility Nothing new Read-only source and provider review, local fixtures. Experiments on a disposable account only with explicit authorization. Official policy stays closed None
P1 publisher evidence Endpoint, keys, snapshot binding, on Fastly and Axum Host-target reference verifier, exposed through operator tooling (proposal). Declaration enrollment and assessments, offline verification Pure verification library, public vectors
P2 deployment appraisal Unchanged Adds read-only Fastly and release collection, fingerprint and qualified official methods, signed assessments and status Same library, official policy only if qualified
P3 operational adoption Unchanged Operated renewal and status service, durable state, checkpoint, isolated signer, publication, recovery runbooks and SLOs Chooses expiry-only or live status
P4 demo registry Fixtures only Illustrative registry UI on fictional data Walkthrough

What the verifier must persist (proposal, derived from the spec's requirements):

  • Enrollment record: the tuple, authorization generation, status sequence, operator authorization record, DNS proof digest and expiry.
  • Records: publisher key records and status, accepted bindings, and the retained policy documents.
  • Signed objects: immutable, as exact bytes, with the exact evidence they examined.
  • Observations: sanitized provider observations.
  • Outstanding nonces: single-use, 60-second life.
  • Outbox.
  • Audit trail: enrollment and recovery events, kept 90 days by default in restricted storage.
  • Recovery checkpoint: outside the backup domain, for the monitored or live-status contract.

11. What this does and doesn't prove

An accepted declaration result proves It does not prove
An independently enrolled key for this exact host answered a fresh challenge recently Which code is installed. That needs the official profile after Fastly qualification. Even then, nothing proves uninterrupted execution at every POP
The declared deployment identity is structurally consistent and authorized for that host and key That the runtime configuration was independently observed, or anything about consent behavior, integration safety or the full configuration
The endorsement is bounded in time and can be withdrawn That the key is bound to the code using it. It is operator-held in every profile, and a fresh answer can't rule out a live relay
Consumers chose this verifier and policy themselves That a bid request naming this domain came from it (a request-signing bridge is optional future work), that Trusted Server is the outermost layer for all site traffic, or anything about the legal entity

Open items:

  • Task-force question 3: persistent operator delegation.
  • Task-force question 7: the request-signing bridge.
  • Task-force question 8: maximum verifier key lifetime and an issuer allowlist record.
  • Official profile: needs the Fastly qualification and a canonical official policy document.
  • Monitored profile: also needs a qualified provider observation method and a qualified recovery checkpoint.

@prk-Jr
prk-Jr marked this pull request as ready for review November 10, 2026 17:24
@prk-Jr
prk-Jr requested review from aram356 and jevansnyc November 10, 2026 17:40

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and prettier --check in docs/. The other comments describe their fix in prose, because the change touches several places or needs regenerated signatures.

Blocking

🔧 wrench

  • httpsIdentifier accepts IP literals: see inline at publisher-attestation-v1.schema.json:98
  • host admits names that URL parsers turn into IPv4 addresses: see inline at publisher-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 tests was 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/trace terminal dispatcher instead of a new route shape: see inline at 2026-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_report is outside the binding, so assessments don't cover it: see inline at 2026-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_id path carries no authority: see inline at 2026-11-04-publisher-attestation-design.md:267
  • How does [attestation].host relate to publisher.domain?: see inline at 2026-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 S and canonical-encoding checks: see inline at 2026-11-04-publisher-attestation-design.md:157
  • The API documents metadata.hashsum as deprecated, not obsolete: see inline at 2026-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 #161 to Part of #161, and update the description to say what approving this revision means.
  • 🔧 browser integration tests was 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, kid selection 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".
  • 🏕 Mark the January draft superseded. docs/superpowers/specs/2026-01-15-attestation-design.md still 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

@prk-Jr
prk-Jr marked this pull request as draft November 11, 2026 14:57
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.
@prk-Jr

prk-Jr commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

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.

@prk-Jr
prk-Jr marked this pull request as ready for review November 11, 2026 15:49
@prk-Jr
prk-Jr requested a review from aram356 November 11, 2026 15:50

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 log section.
  • 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_id is 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 on docs/ 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_id is an unverified label: see inline at 2026-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_r can't catch a missing R canonicality check: see inline at generate.mjs:211
  • non_canonical_operator_identifier now fails for two reasons: see inline at publisher-attestation-v1-vectors.json:2205
  • PR state and Closes #161 still 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 at 2026-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_satisfied result is labelled malformed: see inline at harness.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 policyCheck comment contradicts the registry's method semantics: see inline at publisher-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 #161 still 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 #161 to Part 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 tests and trace runtime and browser acceptance failed at Install Playwright / Install browser dependencies with "Temporary failure resolving 'azure.archive.ubuntu.com' … Failed to install browsers". In the browser job, that skipped Build browser config generator, and the test steps then failed with generate-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-docs step that runs generate.mjs --check and the node tests is missing from three places:
    • AGENTS.md "CI Gates", where it would be item 14, noting that cd docs && npm ci is required for ajv;
    • .claude/commands/check-ci.md;
    • the scripts/README.md table.
  • ⛏ Smaller tooling and spec items.
    • Parser depth bound. harness.test.mjs:56 uses depth > 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 SyntaxError instead of rejecting at stage 1.
    • Point length. point-admission.mjs:32 strictPoint has 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-139 joins 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 in enrollment_txt_value, leaves the node tests green; only generate.mjs --check catches it. Reordered keys pass --check because output keeps input order. Either document that or sort the output.
    • README. README.md:17-18 says to run prettier after generating, but generate.mjs:290-297 already 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_example expectation says "unlisted or invalid kid", but design line 327 maps a malformed kid to invalid_request.
      • Design line 347 assigns the read counts to T5/T7, but the T7 row omits them.
      • The PA-FAILURE table still defines clock_unavailable as "Reliable current time unavailable", contradicting the new definition at line 415.
      • Line 339 calls publisher.domain a "cookie domain", but publisher.cookie_domain is a separate setting (settings.rs:53), and no T-row tests the new host rule.
    • 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 name to_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.

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

@prk-Jr
prk-Jr marked this pull request as draft November 12, 2026 05:45
@prk-Jr

prk-Jr commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

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 Part of #161 and no automatic closing reference. Maintainer spec approval, production implementation and provider/operational qualification remain separate future gates; resolved conversations do not imply reviewer approval or merge readiness.

@prk-Jr
prk-Jr marked this pull request as ready for review November 12, 2026 07:47
@prk-Jr
prk-Jr requested a review from aram356 November 12, 2026 07:47
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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trusted Server attestation

2 participants