From e00e311c92b1627523c9a3f67a1d6ff210baece3 Mon Sep 17 00:00:00 2001 From: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:19:56 -0600 Subject: [PATCH] =?UTF-8?q?docs(spec):=20reconcile=20ACS-Core=20signature?= =?UTF-8?q?=20requirement=20with=20schema=20optionality=20in=20=C2=A710?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns §10, §10.1, conformance.md, core_concepts.md, request-envelope.json, and handshake.json (ServerHello.signature) on ACS-Core signature requirements and schema optionality. Fixes #195 Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> --- docs/spec/conformance.md | 6 ++-- docs/spec/instrument/specification.md | 6 ++-- docs/topics/core_concepts.md | 2 +- .../agt/packages/guardian/test/server.test.ts | 7 +++- specification/v0.1.0/handshake.json | 4 +++ specification/v0.1.0/request-envelope.json | 5 ++- tests/test_response_envelope.py | 33 +++++++++++++++++++ 7 files changed, 54 insertions(+), 9 deletions(-) diff --git a/docs/spec/conformance.md b/docs/spec/conformance.md index 08a9f7a1..174e0234 100644 --- a/docs/spec/conformance.md +++ b/docs/spec/conformance.md @@ -1,6 +1,6 @@ # Conformance Profiles -ACS v0.1.0 conformance is tiered. Every conformant deployment implements **ACS-Core** — the mandatory baseline below. Trace event emission, AgBOM serialization, field-level provenance, cryptographic signatures, and strengthened audit chains are organized as **profiles** that deployments declare independently in the [handshake](./instrument/specification.md#4-capability-negotiation-handshake). +ACS v0.1.0 conformance is tiered. Every conformant deployment implements **ACS-Core** — the mandatory baseline below. Trace event emission, AgBOM serialization, field-level provenance, asymmetric or post-quantum signatures, and strengthened audit chains are organized as **profiles** that deployments declare independently in the [handshake](./instrument/specification.md#4-capability-negotiation-handshake). The profile system lets a small harness ship a useful subset of ACS quickly, while a more capable deployment can layer on observability, supply-chain inventory, or cryptographic integrity without changing the wire format. @@ -33,7 +33,7 @@ A v0.1.0-conformant deployment MUST implement ACS-Core. ACS-Core comprises: - **Dispositions** — Four MUST-support: ALLOW, DENY, ASK, DEFER, with required fields per [§6](./instrument/specification.md#6-disposition-vocabulary). MODIFY is SHOULD-support: deployments that implement MODIFY MUST follow the MODIFY composition rules ([§6.3](./instrument/specification.md#63-modify-composition-normative)); deployments that do not implement MODIFY are handled per [§6.5](./instrument/specification.md#65-modify-incapable-clients-normative) (the Guardian substitutes `DENY` with `reason_codes: ["modify_unsupported"]` and an audit event recording the substitution, determined by deployment-defined means rather than a wire declaration, mirroring the §9.2 precedent for ASK). - **SessionContext and Intent**: `session_id`, `chain_hash` (rolling SHA-256), append-only ContextEntry chain, with the Guardian publishing the chain head (`chain_hash`) on responses for content-bearing steps ([§8](./instrument/specification.md#8-sessioncontext-and-intent)). Intent is optional but normative when IBAC is the enforcement paradigm. - **Replay protection** — `request_id` (UUID) and `timestamp` on every request; Guardians MUST reject replays per [§10.3](./instrument/specification.md#103-replay-protection). -- **Baseline integrity**: every request and response carries a signature over the canonical envelope ([§10](./instrument/specification.md#10-cryptographic-signatures)). `HMAC-SHA256` with an HKDF-derived per-session key from deployment-provided key material is the baseline; asymmetric and post-quantum algorithms are the ACS-Crypto profile. +- **Baseline integrity**: every request and response carries a signature over the canonical envelope, except the envelopes [§10](./instrument/specification.md#10-cryptographic-signatures) exempts. `HMAC-SHA256` with an HKDF-derived per-session key from deployment-provided key material is the baseline; asymmetric and post-quantum algorithms are the ACS-Crypto profile. - **Decision honoring**: the Observed Agent MUST wait for the Guardian's decision up to the negotiated timeout and apply it; on a decision failure (timeout, transport failure, or an error without a decision) it applies the `on_decision_failure` posture (default `proceed`, fail-open) and records every decision failure as an audit event, fail-open or fail-closed ([§6.4](./instrument/specification.md#64-honoring-decisions-normative)). Handshake failure follows the deployment's startup posture ([§4.1](./instrument/specification.md#41-handshake-failure-normative)). - **Liveness** — `system/ping` SHOULD be implemented: the Observed Agent declares it in `methods_implemented` and sends it, and the Guardian answers it per [§13](./instrument/specification.md#13-liveness-system-methods). A deployment MAY omit it only if the Observed Agent relies on an alternative liveness mechanism that the deployment's configuration names (for example: TCP keepalive or HTTP/2 PING on HTTP transports; process supervision or an out-of-band heartbeat on stdio; or continuous observed hook traffic where the session's own cadence acts as the probe). ACS does not put that declaration on the wire in v0.1, as [§9.2](./instrument/specification.md#92-approver-incapable-clients-normative) does not for approver capability. Omitting `system/ping` without a declared alternative is non-conformant: the chain of defaults (`startup_posture: proceed`, `on_decision_failure: proceed`, no liveness) produces silent fail-open on Guardian outage, which the Core baseline cannot allow. - **Wrapped MCP** — `protocols/MCP/*` ([Hooks](./instrument/hooks.md#protocolsmcp)) MUST be implemented by deployments whose sessions involve MCP at any point (tools, resources, prompts, notifications), so that `protocols/MCP/resources/read` — the canonical prompt-injection ingress for attacker-controlled external content — is not bypassed. MCP `tools/call` MAY instead flow through the generic `steps/toolCallRequest` / `steps/toolCallResult` hooks when tool-level policy suffices; the requirement exists for the surfaces the generic hooks cannot see (capability negotiation, prompt fetches, resource reads, notifications). Support is declared in the handshake via `wrapped_protocols`. Deployments whose sessions never involve MCP MAY omit the namespace. **Known v0.1 limitation:** `wrapped_protocols` is declared once, at handshake, and v0.1 defines no renegotiation. A session that declared no MCP and then gains an MCP server mid-session (a mutation `agbom/changed` reports, and the `acs-inspect-dynamic` profile exists to surface) cannot begin wrapping mid-session: the generic tool hooks still govern that server's tool calls, the addition is visible through `agbom/changed`, and wrapped coverage begins at the next session's handshake. Renegotiation is v0.2 scope. @@ -77,7 +77,7 @@ Required for FIDES, CaMeL, and AARM-style enforcement paradigms (which depend on ## ACS-Crypto -Adds cryptographic signature support beyond the baseline's replay-protection fields. A deployment claiming ACS-Crypto MUST support at least `ML-DSA-65` (RECOMMENDED primary) and SHOULD support `SLH-DSA-128s` as an algorithmic-diversity backup. Hybrid composites (`ML-DSA-65+ECDSA-P256`, `ML-DSA-65+RSA-PSS-SHA256`) are OPTIONAL for transitional deployments. +Adds asymmetric and post-quantum signature support beyond the `HMAC-SHA256` baseline. A deployment claiming ACS-Crypto MUST support at least `ML-DSA-65` (RECOMMENDED primary) and SHOULD support `SLH-DSA-128s` as an algorithmic-diversity backup. Hybrid composites (`ML-DSA-65+ECDSA-P256`, `ML-DSA-65+RSA-PSS-SHA256`) are OPTIONAL for transitional deployments. ACS-Core requires a baseline signature; `HMAC-SHA256` over the canonical envelope ([§10](./instrument/specification.md#10-cryptographic-signatures)) satisfies it. ACS-Crypto replaces or augments the baseline with asymmetric and post-quantum algorithms, which add the non-repudiation and external verifiability the symmetric baseline cannot give. Transport-level security alone does not satisfy the Core signature requirement: it does not bind a message for audit, or across multi-hop A2A and multi-Guardian paths. diff --git a/docs/spec/instrument/specification.md b/docs/spec/instrument/specification.md index 12e98bc8..93eba510 100644 --- a/docs/spec/instrument/specification.md +++ b/docs/spec/instrument/specification.md @@ -4,7 +4,7 @@ The Instrument pillar covers real-time interception, evaluation, and enforcement of agent behavior. It defines the wire format, the capability-negotiation handshake, the hook taxonomy, the disposition vocabulary, the SessionContext and Intent model, replay protection, and signature semantics. Trace (event emission) and Inspect (AgBOM) are co-equal v0.1.0 pillars covered in their own pages. -A deployment that implements **ACS-Core** (this section's mandatory baseline) is v0.1.0-conformant. Additional capabilities (Trace event emission, AgBOM serialization, field-level provenance, cryptographic signatures, strengthened audit chains) are organized as **conformance profiles** (see [Conformance](../conformance.md)). +A deployment that implements **ACS-Core** (this section's mandatory baseline) is v0.1.0-conformant. Additional capabilities (Trace event emission, AgBOM serialization, field-level provenance, asymmetric or post-quantum signatures, strengthened audit chains) are organized as **conformance profiles** (see [Conformance](../conformance.md)). ## 1. Design Principles @@ -347,7 +347,7 @@ This rule preserves the security guarantee (actions that would have been `ASK`'d ## 10. Cryptographic Signatures -A signature over the §10 canonical input is REQUIRED in ACS-Core. The signature envelope is `{ algorithm, value, key_id }`, the algorithm drawn from the registry below and the per-direction supported set declared in the handshake. `HMAC-SHA256` is the baseline that satisfies the requirement; ACS-Core does not require asymmetric or post-quantum signatures, which are the ACS-Crypto profile (§10.1). The per-session HMAC key is `HKDF`-derived from deployment-provided input keying material (a pre-shared secret, or a transport channel binding such as a TLS exporter) together with the `session_id`. ACS v0.1 defines no in-band key-exchange: a deployment with neither a shared secret nor a secure transport cannot use the HMAC baseline and satisfies the requirement with an ACS-Crypto algorithm whose key distribution it manages out of band. +A signature over the §10 canonical input is REQUIRED in ACS-Core for every request and response except `system/ping` (§13) and error responses where no signing key can be resolved (such as `SIGNATURE_INVALID` for an unknown `key_id`). `signature` is optional in the base JSON Schemas, so schema validation alone does not establish ACS-Core baseline integrity. The signature envelope is `{ algorithm, value, key_id }`, the algorithm drawn from the registry below and the per-direction supported set declared in the handshake. `HMAC-SHA256` is the baseline that satisfies the requirement; ACS-Core does not require asymmetric or post-quantum signatures, which are the ACS-Crypto profile (§10.1). The per-session HMAC key is `HKDF`-derived from deployment-provided input keying material (a pre-shared secret, or a transport channel binding such as a TLS exporter) together with the `session_id`. ACS v0.1 defines no in-band key-exchange: a deployment with neither a shared secret nor a secure transport cannot use the HMAC baseline and satisfies the requirement with an ACS-Crypto algorithm whose key distribution it manages out of band. **HMAC key derivation.** For `HMAC-SHA256`, the per-session key is HKDF ([RFC 5869](https://www.rfc-editor.org/rfc/rfc5869)) with SHA-256, no salt (RFC 5869's default, a string of 32 zero bytes, which gives the same key as an empty salt), `info` set to the UTF-8 bytes of `metadata.session_id`, and a 32-byte output, applied to the secret that `key_id` names. Both parties MUST derive the key with exactly these parameters, so independent implementations holding the same secret derive the same key. `info` is the `session_id` string exactly as the request carries it, not parsed or normalized first: an uppercase or `urn:uuid:` spelling of the same UUID derives a different key. A response carries no `session_id`, so it is signed and verified with the key of the request it answers. The secret MUST be at least 32 bytes from a cryptographically secure random source. HKDF does no key stretching, so a passphrase used as the secret can be recovered offline from one captured signature. This derivation is defined for a pre-shared secret. v0.1 defines no exporter label or context for a channel-binding input, so that input is not yet interoperable between implementations. For example, the 32-byte secret `000102…1e1f` (the bytes 0 through 31) and the `session_id` `6b1f2c9e-4d7a-4e3b-8a51-9c0d2e7f1a36` give the key `80f0dc8c611279bfc2af2d5f374c5cb685888192ce1af5c3e0673d8fa748e8a4`. @@ -357,7 +357,7 @@ A signature over the §10 canonical input is REQUIRED in ACS-Core. The signature ### 10.1 Algorithm registry -The registry is crypto-agile: the `{ algorithm, value, key_id }` envelope supports any registered algorithm, and the handshake declares which algorithms each side supports. v0.1 prioritizes adoption breadth over cryptographic maximalism: signatures are already field-optional, and a spec that ships without PQC mandates protects more deployments than a spec that mandates PQC and doesn't ship. PQC algorithms from NIST FIPS 203–205 (2024) are registered and available; a future version is expected to promote PQC to RECOMMENDED once ecosystem support matures. +The registry is crypto-agile: the `{ algorithm, value, key_id }` envelope supports any registered algorithm, and the handshake declares which algorithms each side supports. v0.1 prioritizes adoption breadth over cryptographic maximalism: `HMAC-SHA256` satisfies the ACS-Core requirement without asymmetric PKI, and a spec that ships without PQC mandates protects more deployments than a spec that mandates PQC and doesn't ship. PQC algorithms from NIST FIPS 203–205 (2024) are registered and available; a future version is expected to promote PQC to RECOMMENDED once ecosystem support matures. | Algorithm | Class | v0.1.0 status | |---|---|---| diff --git a/docs/topics/core_concepts.md b/docs/topics/core_concepts.md index d4a6027c..c4d02ba8 100644 --- a/docs/topics/core_concepts.md +++ b/docs/topics/core_concepts.md @@ -10,7 +10,7 @@ ACS v0.1.0 organizes capabilities into three co-equal pillars: 2. **Trace**: deterministic event emission. Every hook is also recordable as an OpenTelemetry span and an OCSF event. Decisions are recorded as span events on the parent step span, so the verdict and the action it gates share a parent. See [Trace Events](../spec/trace/events.md). 3. **Inspect**: queryable, dynamic Agent Bill of Materials (AgBOM). The Observed Agent declares its components (models, MCP servers, A2A peers, tools, knowledge sources, memory stores) and reports mutations. See [Inspect](../spec/inspect/README.md). -A v0.1.0-conformant deployment implements **ACS-Core** (the Instrument baseline). Trace, Inspect, field-level Provenance, cryptographic signatures, and strengthened audit chains are organized as [conformance profiles](../spec/conformance.md) declared in the handshake. +A v0.1.0-conformant deployment implements **ACS-Core** (the Instrument baseline). Trace, Inspect, field-level Provenance, asymmetric or post-quantum signatures, and strengthened audit chains are organized as [conformance profiles](../spec/conformance.md) declared in the handshake. ## The two parties diff --git a/reference-implementations/agt/packages/guardian/test/server.test.ts b/reference-implementations/agt/packages/guardian/test/server.test.ts index 60f0be35..49be99ca 100644 --- a/reference-implementations/agt/packages/guardian/test/server.test.ts +++ b/reference-implementations/agt/packages/guardian/test/server.test.ts @@ -19,17 +19,22 @@ import type { AcsFinalResult } from "../src/acs-result.ts"; // The repository's own handshake schema, five directories up from this // file: this tree sits at reference-implementations/agt. const HANDSHAKE_SCHEMA_PATH = fileURLToPath(new URL("../../../../../specification/v0.1.0/handshake.json", import.meta.url)); +const REQUEST_ENVELOPE_SCHEMA_PATH = fileURLToPath(new URL("../../../../../specification/v0.1.0/request-envelope.json", import.meta.url)); /** Compiles the ServerHello $def straight out of the pinned handshake.json -- * not a hand-copied shape -- so this test fails the moment our ServerHello * drifts from the schema. */ function validateServerHello(candidate: unknown): void { const handshakeSchema = JSON.parse(readFileSync(HANDSHAKE_SCHEMA_PATH, "utf8")) as { + $id: string; $defs: { ServerHello: Record }; }; + const requestEnvelopeSchema = JSON.parse(readFileSync(REQUEST_ENVELOPE_SCHEMA_PATH, "utf8")) as Record; const ajv = new Ajv2020({ strict: true, allErrors: true }); addFormats(ajv); - const validate = ajv.compile(handshakeSchema.$defs.ServerHello); + ajv.addSchema(requestEnvelopeSchema); + ajv.addSchema(handshakeSchema); + const validate = ajv.getSchema(`${handshakeSchema.$id}#/$defs/ServerHello`)!; const valid = validate(candidate); if (!valid) { throw new Error(`ServerHello failed schema validation: ${JSON.stringify(validate.errors)}`); diff --git a/specification/v0.1.0/handshake.json b/specification/v0.1.0/handshake.json index c901f94a..9ab34581 100644 --- a/specification/v0.1.0/handshake.json +++ b/specification/v0.1.0/handshake.json @@ -163,6 +163,10 @@ "enum": ["acs-core", "acs-trace", "acs-inspect", "acs-inspect-dynamic", "acs-provenance", "acs-crypto", "acs-audit"] }, "description": "Conformance profiles accepted for this session. The Guardian selects from the client's profiles_supported based on its own capabilities and policy requirements. A Guardian MAY refuse a session if the client does not declare a profile the Guardian's policy requires." + }, + "signature": { + "$ref": "request-envelope.json#/$defs/Signature", + "description": "Signature over this response envelope, computed on the canonical input defined in Specification 10 (the JCS canonicalization of the envelope with the signature field removed). Optional in the schema; mandatory under ACS-Core." } } } diff --git a/specification/v0.1.0/request-envelope.json b/specification/v0.1.0/request-envelope.json index 2b851a81..c99e5ef8 100644 --- a/specification/v0.1.0/request-envelope.json +++ b/specification/v0.1.0/request-envelope.json @@ -54,7 +54,10 @@ "type": "object", "description": "Hook-specific payload. Schema depends on method." }, - "signature": { "$ref": "#/$defs/Signature" } + "signature": { + "$ref": "#/$defs/Signature", + "description": "Signature over this request envelope, computed on the canonical input defined in Specification 10 (the JCS canonicalization of the envelope with the signature field removed). Optional in the schema (to accommodate system/ping, Specification 13); mandatory under ACS-Core." + } } }, "Metadata": { diff --git a/tests/test_response_envelope.py b/tests/test_response_envelope.py index 13c91359..017b2274 100644 --- a/tests/test_response_envelope.py +++ b/tests/test_response_envelope.py @@ -71,3 +71,36 @@ def test_a_server_hello_missing_a_required_field_is_rejected(): """The ServerHello branch is checked against handshake.json, not merely permitted.""" partial = {key: value for key, value in SERVER_HELLO.items() if key != "timeout_config"} assert not _response_envelope_validator().is_valid(_envelope(partial)) + +def test_signature_requirement_and_schema_optionality_are_consistent(): + """Issue #195: section 10 states the rule, and the schemas and conformance.md agree with it.""" + root = SPEC.parents[1] + spec_text = (root / "docs/spec/instrument/specification.md").read_text(encoding="utf-8") + conformance = (root / "docs/spec/conformance.md").read_text(encoding="utf-8") + concepts = (root / "docs/topics/core_concepts.md").read_text(encoding="utf-8") + req = json.loads((SPEC / "request-envelope.json").read_text(encoding="utf-8")) + resp = json.loads((SPEC / "response-envelope.json").read_text(encoding="utf-8")) + handshake = json.loads((SPEC / "handshake.json").read_text(encoding="utf-8")) + + req_sig = req["$defs"]["AcsParams"]["properties"]["signature"] + resp_sig = resp["$defs"]["AcsResult"]["properties"]["signature"] + hello_sig = handshake["$defs"]["ServerHello"]["properties"]["signature"] + assert req_sig["$ref"] == "#/$defs/Signature" + assert resp_sig["$ref"] == "request-envelope.json#/$defs/Signature" + assert hello_sig["$ref"] == "request-envelope.json#/$defs/Signature" + assert "signature" not in req["$defs"]["AcsParams"]["required"] + assert "signature" not in resp["$defs"]["AcsResult"]["required"] + assert "signature" not in handshake["$defs"]["ServerHello"]["required"] + for description in (req_sig["description"], resp_sig["description"], hello_sig["description"]): + assert "Optional in the schema" in description + assert "mandatory under ACS-Core" in description + + section_10 = spec_text.split("## 10. Cryptographic Signatures")[1].split("### 10.2")[0] + assert "is REQUIRED in ACS-Core for every request and response except" in section_10 + assert "optional in the base JSON Schemas" in section_10 + assert "control traffic" not in section_10 + assert "field-optional" not in spec_text + assert "except the envelopes [§10]" in conformance + for text in (spec_text, conformance, concepts): + assert "cryptographic signatures" not in text +