id: signed_requests
version: "1.0.0"
title: "Signed requests — RFC 9421 transport-layer verification"
category: signed_requests
summary: "Agent verifies RFC 9421 HTTP Signatures on incoming AdCP requests per the 3.1-compatible transport-layer profile. Universal capability-gated storyboard — runs for any agent advertising `request_signing.supported: true` regardless of `supported_protocols`. Graded against the legacy wire suite covering every 3.1 checklist step and canonicalization-edge rule."
track: security_transport
required_tools:
  - get_adcp_capabilities
  # Additional tool families are exercised transport-layer only — the runner signs
  # requests targeting any mutating AdCP operation (create_media_buy, update_media_buy,
  # acquire_*, activate_signal, sync_creatives) and grades the verifier's response.
  # The runner picks operations available given the agent's `supported_protocols`;
  # operations the agent does not implement are skipped, not failed. Runner
  # implementation tracked at adcontextprotocol/adcp#2331.

narrative: |
  Request signing is a transport-layer claim that applies to every AdCP agent regardless
  of which protocols it speaks. RFC 9421 verification behavior is identical whether the
  agent is a media-buy seller, a signals agent, a governance agent, a creative agent, or
  a brand agent. This universal storyboard certifies that an agent correctly implements
  the AdCP RFC 9421 request-signing verifier per
  [`docs/building/by-layer/L1/security.mdx#signed-requests-transport-layer`](/docs/building/by-layer/L1/security#signed-requests-transport-layer).

  **Gating.** This storyboard runs for any agent advertising `request_signing.supported: true`
  in `get_adcp_capabilities`. Agents that do not advertise support are not tested against
  this storyboard — absence of advertisement is not a failure, it is a declaration that
  the agent does not offer verified signed requests. Same gating model as
  `deterministic_testing` (gated on `compliance_testing.supported: true`).

  **Backward-compatible specialism claims.** The deprecated `signed-requests` specialism
  enum value remains in the schema for back-compat (see
  [#3075](https://github.com/adcontextprotocol/adcp/issues/3075)). Agents that still
  advertise `specialisms: ["signed-requests"]` are graded via this universal storyboard;
  the runner SHOULD emit the `signed_requests_specialism_deprecated` notice (severity
  `deprecation`, see `runner-output-contract.yaml` > `notice` > `canonical_codes`) advising
  the agent to drop the now-redundant specialism claim and rely solely on
  `request_signing.supported: true`. The notice MUST NOT change `step_result.passed` —
  it is an advisory, not a failure. 4.0 removes the enum value entirely — see
  [#3078](https://github.com/adcontextprotocol/adcp/issues/3078).

  **Composition with other storyboards.** This storyboard runs independently of any
  specialism a seller also claims. A seller advertising `request_signing.supported: true`
  AND `specialisms: ["sales-guaranteed"]` is graded on both — universal failure does not
  exempt the specialism's pass, and a specialism pass does not paper over universal
  failure. The overall AAO Verified verdict requires all gated storyboards to pass; the
  runner reports per-storyboard outcomes so operators can isolate failures.

  **Grading.** Observable-behavior only. The runner constructs signed HTTP requests
  exactly as documented in the conformance vectors at
  `/compliance/{version}/test-vectors/request-signing/` and sends them to the agent. The
  agent's responses are compared against the vectors' `expected_outcome`:

  This storyboard's current black-box runner contract is 3.1-compatible. It does not
  claim 3.2 certification. The repository CI reference evaluator covers the 3.2
  body-integrity policy while runner-side profile selection is added.

  - Positive vectors MUST produce a non-4xx response — the agent accepted the signed request.
  - Negative vectors MUST produce `401` with `WWW-Authenticate: Signature error="<code>"`,
    where the `<code>` matches the vector's `expected_outcome.error_code` byte-for-byte.
    The checklist step number is informational; grading is on the stable error code only.

  The runner supplies signed requests; the agent is the verifier. A separate signer-side
  storyboard may follow but is out of scope here — signers are easier to get right, and a
  signer that produces valid signatures is proven correct by a conformant verifier
  accepting them.

  **Stateful vectors.** Three negative vectors — 016 (replayed nonce), 017 (key revoked),
  and 020 (per-keyid cap) — assert verifier state the runner cannot set from the outside.
  Those vectors declare `requires_contract` and are gated on the `signed_requests_runner`
  test-kit contract at `test-kits/signed-requests-runner.yaml`, which specifies the
  runner's signing keyids, a dedicated pre-revoked keyid (`test-revoked-2026`), and the
  grading-time per-keyid cap the runner will target. Agents advertising
  `request_signing.supported: true` MUST pre-configure their verifier per that contract
  before the negative phase runs. If the agent does not satisfy the contract (e.g., the
  runner can't coordinate the pre-state), the affected vectors grade as FAIL, never as
  SKIP — the contract is a prerequisite for the capability claim, not an optional
  extension.

agent:
  interaction_model: request_verifier
  capabilities:
    - verifies_request_signatures
  examples:
    - "Any AdCP agent advertising `request_signing.supported: true` — sales agent, signals agent, governance agent, creative agent, or brand agent"

caller:
  role: compliance_runner
  example: "AdCP Verified runner (or equivalent signed-request grader)"

prerequisites:
  description: |
    The agent MUST advertise `request_signing.supported: true` in `get_adcp_capabilities`.
    The agent's verifier MUST be configured to accept requests signed by the test keypairs
    published in `keys.json` alongside the conformance vectors — specifically, the runner's
    signing keys (`test-ed25519-2026`, `test-es256-2026`) should be treated as coming from
    a registered test counterparty whose JWKS contains those public keys with
    `adcp_use: "request-signing"`.
  test_kit: "test-kits/signed-requests-runner.yaml"

phases:
  - id: capability_discovery
    title: "Capability discovery"
    narrative: |
      Confirm the agent advertises request-signing support before grading the verifier.

    steps:
      - id: get_capabilities
        title: "Verify the agent declares request_signing.supported"
        narrative: |
          The runner calls `get_adcp_capabilities` and confirms that the agent declares
          support for the request-signing capability. Absence of `request_signing.supported: true`
          means the agent has not opted into this storyboard; the runner SHOULD skip the
          remaining phases.
        task: get_adcp_capabilities
        schema_ref: "protocol/get-adcp-capabilities-request.json"
        response_schema_ref: "protocol/get-adcp-capabilities-response.json"
        doc_ref: "/protocol/get_adcp_capabilities"
        comply_scenario: capability_discovery
        stateful: false
        expected: |
          Return capabilities declaring request_signing.supported: true. If false or absent,
          this storyboard does not apply and grading skips to a non-applicable outcome.
        sample_request:
          context:
            correlation_id: "signed_requests--get_capabilities"
        validations:
          - check: field_present
            path: "request_signing"
            description: "Agent declares request_signing capability block"
          - check: field_value
            path: "request_signing.supported"
            value: true
            description: "Agent verifies request signatures"

  - id: positive_vectors
    title: "Positive conformance vectors"
    narrative: |
      The runner sends each positive vector as a signed HTTP request to the agent and
      expects the agent to accept it. Vectors exercise the happy path plus every
      canonicalization-edge rule in the profile:

        001 basic Ed25519 POST
        002 Ed25519 POST with content-digest covered
        003 ES256 POST (edge-runtime profile)
        004 multiple Signature-Input labels (verifier processes sig1 only)
        005 default-port strip (URL has :443)
        006 dot-segment path normalization
        007 query-string byte preservation (no alphabetization)
        008 percent-encoded path normalization (lowercase → uppercase hex)

      Vectors and test keys live at
      `/compliance/{version}/test-vectors/request-signing/positive/` and
      `/compliance/{version}/test-vectors/request-signing/keys.json`.

    $comment: |
      Step-level YAML entries for each positive vector will be generated from the vector
      fixtures at runtime by the compliance runner; we do not duplicate them in this file.
      The runner loads the vector, constructs the HTTP request, sends it to the agent,
      and validates the response against expected_outcome.success. Runner implementation
      tracked at adcontextprotocol/adcp#2331.

  - id: negative_vectors
    title: "Negative conformance vectors"
    narrative: |
      The runner sends each negative vector and expects the agent to reject with the
      stable error code in `expected_outcome.error_code`. Negative vectors cover every
      failure mode in the verifier checklist (13 numbered steps plus sub-step 9a for
      the per-keyid cap) plus the pre-check:

        001 unsigned + required_for           → request_signature_required
        002 wrong tag                         → request_signature_tag_invalid
        003 expired signature                 → request_signature_window_invalid
        004 window > 300s                     → request_signature_window_invalid
        005 alg not allowed                   → request_signature_alg_not_allowed
        006 missing covered component         → request_signature_components_incomplete
        007 missing content-digest (required) → request_signature_components_incomplete
        008 unknown keyid                     → request_signature_key_unknown
        009 wrong adcp_use                    → request_signature_key_purpose_invalid
        010 content-digest mismatch           → request_signature_digest_mismatch
        011 malformed Signature-Input         → request_signature_header_malformed
        012 missing expires param             → request_signature_params_incomplete
        013 expires ≤ created                 → request_signature_window_invalid
        014 missing nonce param               → request_signature_params_incomplete
        015 signature cryptographically invalid → request_signature_invalid
        016 replayed nonce                    → request_signature_replayed
        017 key revoked                       → request_signature_key_revoked
        018 content-digest when forbidden     → request_signature_components_unexpected
        019 Signature without Signature-Input → request_signature_header_malformed
        020 per-keyid replay cache cap        → request_signature_rate_abuse
        028 unsigned tasks/cancel + protocol_methods_required_for → request_signature_required

      Vector 028 is gated on `request_signing.protocol_methods_required_for`
      being non-empty: the runner skips when the agent does not declare the
      bucket, and FAILs (not SKIPs) when it declares it but doesn't enforce.
      It's the only vector that targets a JSON-RPC protocol method rather
      than a `tools/call` AdCP envelope. Vectors 021-027 (enumerated in the
      negative/ directory but not in the summary above) cover later
      additions to the checklist that share the `tools/call` envelope shape
      with vectors 001-020.

      Vectors live at `/compliance/{version}/test-vectors/request-signing/negative/`.
      Grading requires byte-for-byte match on the error code; the failed_step value is
      informational. The runner reads the error code from the agent's
      `WWW-Authenticate: Signature error="<code>"` response header on 401.

    $comment: |
      As with positive_vectors, step-level entries are synthesized by the runner from
      the vector fixtures. Runner implementation at adcontextprotocol/adcp#2331.

grading:
  pass_criteria: |
    All vectors in positive/ produce 2xx, and all vectors in negative/ produce 401 with
    the expected error code byte-for-byte. Any single vector's failure to produce its
    expected outcome fails the storyboard. Vectors that declare `requires_contract`
    whose contract the runner cannot satisfy grade as FAIL, never as SKIP — the
    signed-requests-runner contract is a prerequisite for the capability claim, not an
    optional extension.
  report_format: |
    Pass/fail per vector, with the response's error code (if any) and a diff against
    the vector's expected_outcome. Operators receive specific per-vector diagnostics
    so they can debug without re-running the full suite.
