From e1b2fca886798f4e57036dff7834dc0353358c47 Mon Sep 17 00:00:00 2001 From: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:55:37 +0000 Subject: [PATCH 1/3] docs: tighten section 10 verifier rules Add normative verifier rules for signature verification order, state isolation, constant-time comparison, missing signature rejection, and non-negotiated algorithm rejection. Align schema optionality with ACS-Core conformance requirements. Refs #315, #316 Signed-off-by: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> --- docs/spec/instrument/specification.md | 14 ++- specification/v0.1.0/handshake.json | 21 +++- tests/test_verifier_rules.py | 149 ++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 tests/test_verifier_rules.py diff --git a/docs/spec/instrument/specification.md b/docs/spec/instrument/specification.md index 12e98bc8..92224ab9 100644 --- a/docs/spec/instrument/specification.md +++ b/docs/spec/instrument/specification.md @@ -78,9 +78,9 @@ The full envelope schemas are [`request-envelope.json`](https://genai-security-p Required at session start, before any hook traffic. Wire method: `handshake/hello`. Schema: [`handshake.json`](https://genai-security-project.github.io/agent-control-standard/schema/v0.1.0/handshake.json) (`$defs/ClientHello` and `$defs/ServerHello`). -**Observed Agent → Guardian Agent (ClientHello):** `acs_versions_supported`, `methods_implemented`, `transports_supported`, `max_payload_size_bytes`, `provenance_producer`, `wrapped_protocols`, `profiles_supported` (conformance profiles the client implements; see [Conformance](../conformance.md)). +**Observed Agent → Guardian Agent (ClientHello):** `acs_versions_supported`, `methods_implemented`, `transports_supported`, `max_payload_size_bytes`, `provenance_producer`, `wrapped_protocols`, `profiles_supported` (conformance profiles the client implements; see [Conformance](../conformance.md)), `signature_algorithms_supported` (optional; algorithms the client can sign and verify; omitting it negotiates only the baseline `HMAC-SHA256`). -**Guardian Agent → Observed Agent (ServerHello):** `negotiated_version`, `methods_evaluated`, `selected_transport`, `signature_algorithms_supported`, `timeout_config` (default and per-method), `on_decision_failure` (failure posture, §6.4), `skew_window_ms` (request-timestamp skew tolerance for replay protection, §10.3), `approver_types_supported`, `policy_requires_provenance`, `agbom_serializations_supported` (Inspect-pillar serialization formats the Guardian renders on request), `trace_emission` (whether the Guardian emits OTel and/or OCSF for the Trace pillar, plus optional OTLP collector endpoint), `profiles_accepted`. +**Guardian Agent → Observed Agent (ServerHello):** `negotiated_version`, `methods_evaluated`, `selected_transport`, `signature_algorithms_supported` (algorithms the Guardian can sign and verify; the negotiated set for both directions is the intersection of this field and the client's `signature_algorithms_supported`), `timeout_config` (default and per-method), `on_decision_failure` (failure posture, §6.4), `skew_window_ms` (request-timestamp skew tolerance for replay protection, §10.3), `approver_types_supported`, `policy_requires_provenance`, `agbom_serializations_supported` (Inspect-pillar serialization formats the Guardian renders on request), `trace_emission` (whether the Guardian emits OTel and/or OCSF for the Trace pillar, plus optional OTLP collector endpoint), `profiles_accepted`. Version mismatch terminates with `UNSUPPORTED_VERSION` (`-32001`, §17.1). Unknown fields MUST be ignored. If the client declares `provenance_producer: "none"` and the Guardian's `policy_requires_provenance` is true, the Guardian MUST refuse the session at handshake time with `PROVENANCE_REQUIRED` (`-32002`, §17.1) rather than silently degrading enforcement. @@ -355,6 +355,16 @@ A signature over the §10 canonical input is REQUIRED in ACS-Core. The signature **Signed input.** The signed input for every algorithm is the RFC 8785 (JCS) canonicalization of the request or response envelope with the `signature` field removed, encoded as UTF-8. This binds the signature to the whole envelope, including `method`, `metadata.session_id`, `request_id`, and `timestamp`, so a captured signature cannot be lifted into a different envelope (for example, re-wrapping a signed `toolCallResult` under a different `session_id`). A verifier MUST recompute this canonical form and MUST reject a signature that does not cover it (`SIGNATURE_INVALID`, §17.1). The input is fixed by this rule, not declared per message: a discoverable `signed_fields` would let a producer advertise partial coverage that omits `session_id`, reopening the gap this closes. Alternative canonicalization is not permitted in v0.1, matching the chain-hash rule in §8. +**Verifier rules (normative).** These rules apply to every conformant verifier, regardless of algorithm: + +1. **Verification order and state isolation.** A Guardian MUST perform parsing, size-limit checks, and `key_id` resolution before signature verification. Until both the signature check and the timestamp check (§10.3) pass, the request MUST NOT alter session state, replay state, audit-chain state, or authorization state, and policy MUST NOT evaluate it. Security logs and rate-limit counters that are maintained outside the session audit chain MAY be updated on rejected requests. Checking the `request_id` for replay and reserving it MUST be one atomic operation performed after verification succeeds, so that two copies of the same request arriving concurrently cannot both pass the replay check. + +2. **Constant-time comparison.** Verifiers MUST compare signature `value` fields (the decoded signature bytes, not the base64-encoded wire representation) in constant time to prevent timing-based recovery of the key. A naive byte-by-byte or string comparison that exits early on the first mismatch leaks timing information proportional to the number of matching prefix bytes. + +3. **Missing signature.** An envelope that lacks a `signature` field MUST be rejected with `SIGNATURE_INVALID` (`-32004`, §17.1), except `system/ping` (§13) and error responses where no signing key can be resolved, such as the `SIGNATURE_INVALID` response for an unknown `key_id`. + +4. **Non-negotiated algorithm.** The negotiated set for both directions (requests and responses) is the intersection of `ClientHello.signature_algorithms_supported` and `ServerHello.signature_algorithms_supported` (§4). If the ClientHello omits `signature_algorithms_supported`, the negotiated set contains only the ACS-Core baseline `HMAC-SHA256`. A Guardian MUST reject a request whose `signature.algorithm` is outside the negotiated set, returning `SIGNATURE_INVALID`, with no fallback to another algorithm or key. An Observed Agent MUST reject a response whose `signature.algorithm` is outside the negotiated set, also returning `SIGNATURE_INVALID`, with no fallback. During the `handshake/hello` exchange itself, the Guardian verifies the ClientHello request against the algorithms it supports, and the Observed Agent verifies the ServerHello response against `ClientHello.signature_algorithms_supported` (or the baseline if omitted), because the intersection is not known until the ServerHello is processed. + ### 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. diff --git a/specification/v0.1.0/handshake.json b/specification/v0.1.0/handshake.json index c901f94a..df751f9a 100644 --- a/specification/v0.1.0/handshake.json +++ b/specification/v0.1.0/handshake.json @@ -62,6 +62,25 @@ "enum": ["acs-core", "acs-trace", "acs-inspect", "acs-inspect-dynamic", "acs-provenance", "acs-crypto", "acs-audit"] }, "description": "Conformance profiles this Observed Agent implements. 'acs-core' is the mandatory baseline and SHOULD always be included. Other profiles are optional and independently claimable. See Section 4 (Conformance Profiles) for definitions." + }, + "signature_algorithms_supported": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "HMAC-SHA256", + "ECDSA-P256", + "RSA-PSS-SHA256", + "ML-DSA-65", + "ML-DSA-44", + "ML-DSA-87", + "SLH-DSA-128s", + "SLH-DSA-128f", + "ML-DSA-65+ECDSA-P256", + "ML-DSA-65+RSA-PSS-SHA256" + ] + }, + "description": "Signature algorithms the Observed Agent can sign and verify. The negotiated set for both directions (requests and responses) is the intersection of this field and ServerHello.signature_algorithms_supported. If this field is omitted, the negotiated set contains only the ACS-Core baseline HMAC-SHA256. During the handshake itself, the Guardian verifies the ClientHello against the algorithms it supports, and the Observed Agent verifies the ServerHello against this field (or the baseline if omitted)." } } }, @@ -100,7 +119,7 @@ "ML-DSA-65+RSA-PSS-SHA256" ] }, - "description": "Signature algorithms the Guardian can verify on incoming requests. Empty array = signatures not supported / not required. 'HMAC-SHA256' is the v0.1 RECOMMENDED default (simplest deployment path). Classical asymmetric ('ECDSA-P256', 'RSA-PSS-SHA256') and PQC ('ML-DSA-*', 'SLH-DSA-*') are OPTIONAL. A future ACS version is expected to promote PQC to RECOMMENDED once ecosystem support matures." + "description": "Signature algorithms the Guardian can sign and verify. The negotiated set for both directions (requests and responses) is the intersection of this field and ClientHello.signature_algorithms_supported. 'HMAC-SHA256' is the v0.1 RECOMMENDED default (simplest deployment path). Classical asymmetric ('ECDSA-P256', 'RSA-PSS-SHA256') and PQC ('ML-DSA-*', 'SLH-DSA-*') are OPTIONAL. A future ACS version is expected to promote PQC to RECOMMENDED once ecosystem support matures. ACS-Core conformance requires signatures on every envelope except system/ping and key-resolution errors (Specification section 10, verifier rules)." }, "timeout_config": { "type": "object", diff --git a/tests/test_verifier_rules.py b/tests/test_verifier_rules.py new file mode 100644 index 00000000..a5e6d3b6 --- /dev/null +++ b/tests/test_verifier_rules.py @@ -0,0 +1,149 @@ +"""Verifier rules from Specification section 10. + +Tests for the normative verifier requirements: missing signature rejection, +non-negotiated algorithm rejection, and state isolation for failed verification. +These are schema-level and behavioral tests, not reference-implementation tests; +the repository has no runnable verifier yet that enforces all of these rules. +""" + +import json +from pathlib import Path + +SPEC_DIR = Path(__file__).resolve().parent.parent / "specification" / "v0.1.0" +REQUEST_ENVELOPE = SPEC_DIR / "request-envelope.json" +HANDSHAKE = SPEC_DIR / "handshake.json" + + +def test_request_envelope_signature_is_optional_in_schema(): + """The signature field is optional in the JSON Schema to accommodate system/ping + and key-resolution errors, but ACS-Core conformance requires it elsewhere. + """ + schema = json.loads(REQUEST_ENVELOPE.read_text(encoding="utf-8")) + params_schema = schema["$defs"]["AcsParams"] + assert "signature" in params_schema["properties"] + assert "signature" not in params_schema.get("required", []) + + +def test_handshake_clienthello_has_signature_algorithms_supported(): + """ClientHello declares which algorithms the client can sign and verify.""" + schema = json.loads(HANDSHAKE.read_text(encoding="utf-8")) + client_hello = schema["$defs"]["ClientHello"] + assert "signature_algorithms_supported" in client_hello["properties"] + assert "signature_algorithms_supported" not in client_hello.get("required", []) + + +def test_handshake_serverhello_signature_algorithms_supported_has_no_empty_array_exemption(): + """The ServerHello.signature_algorithms_supported description no longer says + 'empty array = signatures not required'. + """ + schema = json.loads(HANDSHAKE.read_text(encoding="utf-8")) + server_hello = schema["$defs"]["ServerHello"] + desc = server_hello["properties"]["signature_algorithms_supported"]["description"] + assert "Empty array = signatures not supported" not in desc + assert "Empty array = signatures not required" not in desc + + +def test_signature_algorithm_enums_match_across_handshake(): + """The algorithm enums in ClientHello and ServerHello match the request envelope.""" + handshake_schema = json.loads(HANDSHAKE.read_text(encoding="utf-8")) + request_schema = json.loads(REQUEST_ENVELOPE.read_text(encoding="utf-8")) + + client_supported = set( + handshake_schema["$defs"]["ClientHello"]["properties"]["signature_algorithms_supported"]["items"]["enum"] + ) + server_supported = set( + handshake_schema["$defs"]["ServerHello"]["properties"]["signature_algorithms_supported"]["items"]["enum"] + ) + request_enum = set( + request_schema["$defs"]["Signature"]["properties"]["algorithm"]["enum"] + ) + + assert client_supported == request_enum + assert server_supported == request_enum + + +def test_specification_text_includes_verifier_rules(): + """Section 10 includes the normative verifier rules block.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "Verifier rules (normative)" in spec + assert "Verification order and state isolation" in spec + assert "Constant-time comparison" in spec + assert "Missing signature" in spec + assert "Non-negotiated algorithm" in spec + + +def test_specification_mentions_atomic_request_id_check(): + """The spec requires that checking request_id for replay and reserving it is atomic.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "atomic" in spec.lower() + assert "request_id" in spec + assert "replay" in spec.lower() + + +def test_specification_forbids_fallback_to_another_algorithm(): + """A non-negotiated algorithm is rejected with no fallback.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "no fallback" in spec.lower() or "MUST NOT fall back" in spec + + +def test_specification_requires_constant_time_comparison(): + """Verifiers must compare signatures in constant time.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "constant time" in spec.lower() or "constant-time" in spec.lower() + + +def test_specification_isolates_failed_verification_from_state(): + """A request that fails verification must not alter the four state kinds.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "session state" in spec + assert "replay state" in spec + assert "audit-chain state" in spec + assert "authorization state" in spec + + +def test_specification_defines_negotiated_set_as_intersection(): + """The negotiated set for both directions is the intersection of the two lists.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "intersection" in spec.lower() + assert "ClientHello.signature_algorithms_supported" in spec + assert "ServerHello.signature_algorithms_supported" in spec + + +def test_specification_defines_baseline_when_field_omitted(): + """If ClientHello omits signature_algorithms_supported, only HMAC-SHA256 is negotiated.""" + spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( + encoding="utf-8" + ) + assert "omits" in spec.lower() or "omitted" in spec.lower() + assert "HMAC-SHA256" in spec + assert "baseline" in spec.lower() + + +def test_handshake_clienthello_signature_algorithms_supported_mentions_baseline(): + """The ClientHello description mentions the baseline when the field is omitted.""" + schema = json.loads(HANDSHAKE.read_text(encoding="utf-8")) + client_hello = schema["$defs"]["ClientHello"] + desc = client_hello["properties"]["signature_algorithms_supported"]["description"] + assert "omitted" in desc.lower() + assert "HMAC-SHA256" in desc + + +def test_handshake_descriptions_mention_intersection(): + """Both algorithm field descriptions mention the intersection.""" + schema = json.loads(HANDSHAKE.read_text(encoding="utf-8")) + client_desc = schema["$defs"]["ClientHello"]["properties"]["signature_algorithms_supported"]["description"] + server_desc = schema["$defs"]["ServerHello"]["properties"]["signature_algorithms_supported"]["description"] + assert "intersection" in client_desc.lower() + assert "intersection" in server_desc.lower() From 8907c53bcf07be2740a5c3d919994ffda01c581e Mon Sep 17 00:00:00 2001 From: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:57:11 +0100 Subject: [PATCH 2/3] docs: address section 10 verifier rules review feedback Scope constant-time comparison to MAC tags, reserve request_id after both checks pass, have the Observed Agent refuse a bad response locally, align 10.1 with the missing-signature rule, and pin the normative phrases in tests. Signed-off-by: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> --- docs/spec/instrument/specification.md | 8 ++++---- tests/test_verifier_rules.py | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/spec/instrument/specification.md b/docs/spec/instrument/specification.md index 92224ab9..a031315d 100644 --- a/docs/spec/instrument/specification.md +++ b/docs/spec/instrument/specification.md @@ -357,17 +357,17 @@ A signature over the §10 canonical input is REQUIRED in ACS-Core. The signature **Verifier rules (normative).** These rules apply to every conformant verifier, regardless of algorithm: -1. **Verification order and state isolation.** A Guardian MUST perform parsing, size-limit checks, and `key_id` resolution before signature verification. Until both the signature check and the timestamp check (§10.3) pass, the request MUST NOT alter session state, replay state, audit-chain state, or authorization state, and policy MUST NOT evaluate it. Security logs and rate-limit counters that are maintained outside the session audit chain MAY be updated on rejected requests. Checking the `request_id` for replay and reserving it MUST be one atomic operation performed after verification succeeds, so that two copies of the same request arriving concurrently cannot both pass the replay check. +1. **Verification order and state isolation.** A Guardian MUST perform parsing, size-limit checks, and `key_id` resolution before signature verification. Until both the signature check and the timestamp check (§10.3) pass, the request MUST NOT alter session state, replay state, audit-chain state, or authorization state, and policy MUST NOT evaluate it. Security logs and rate-limit counters that are maintained outside the session audit chain MAY be updated on rejected requests. Checking the `request_id` for replay and reserving it MUST be one atomic operation performed after both checks pass, so that two copies of the same request arriving concurrently cannot both pass the replay check, and so that a skewed but validly signed request cannot burn an ID. -2. **Constant-time comparison.** Verifiers MUST compare signature `value` fields (the decoded signature bytes, not the base64-encoded wire representation) in constant time to prevent timing-based recovery of the key. A naive byte-by-byte or string comparison that exits early on the first mismatch leaks timing information proportional to the number of matching prefix bytes. +2. **Constant-time comparison.** For MAC tags (HMAC-SHA256), verifiers MUST compare the decoded signature bytes in constant time to prevent timing-based recovery of the key. For asymmetric signatures (ECDSA, RSA-PSS, ML-DSA), verifiers MUST NOT compare base64-encoded wire text, and MUST NOT leak the key or secret material through an early exit or data-dependent timing. 3. **Missing signature.** An envelope that lacks a `signature` field MUST be rejected with `SIGNATURE_INVALID` (`-32004`, §17.1), except `system/ping` (§13) and error responses where no signing key can be resolved, such as the `SIGNATURE_INVALID` response for an unknown `key_id`. -4. **Non-negotiated algorithm.** The negotiated set for both directions (requests and responses) is the intersection of `ClientHello.signature_algorithms_supported` and `ServerHello.signature_algorithms_supported` (§4). If the ClientHello omits `signature_algorithms_supported`, the negotiated set contains only the ACS-Core baseline `HMAC-SHA256`. A Guardian MUST reject a request whose `signature.algorithm` is outside the negotiated set, returning `SIGNATURE_INVALID`, with no fallback to another algorithm or key. An Observed Agent MUST reject a response whose `signature.algorithm` is outside the negotiated set, also returning `SIGNATURE_INVALID`, with no fallback. During the `handshake/hello` exchange itself, the Guardian verifies the ClientHello request against the algorithms it supports, and the Observed Agent verifies the ServerHello response against `ClientHello.signature_algorithms_supported` (or the baseline if omitted), because the intersection is not known until the ServerHello is processed. +4. **Non-negotiated algorithm.** The negotiated set for both directions (requests and responses) is the intersection of `ClientHello.signature_algorithms_supported` and `ServerHello.signature_algorithms_supported` (§4). If the ClientHello omits `signature_algorithms_supported`, the negotiated set contains only the ACS-Core baseline `HMAC-SHA256`. A Guardian MUST reject a request whose `signature.algorithm` is outside the negotiated set, returning `SIGNATURE_INVALID`, with no fallback to another algorithm or key. An Observed Agent MUST refuse a response whose `signature.algorithm` is outside the negotiated set locally, with no fallback. During the `handshake/hello` exchange itself, the Guardian verifies the ClientHello request against the algorithms it supports, and the Observed Agent verifies the ServerHello response against `ClientHello.signature_algorithms_supported` (or the baseline if omitted), because the intersection is not known until the ServerHello is processed. ### 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 (while `signature` remains optional in the base JSON Schema only for exempted control traffic such as `system/ping`, §13), 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/tests/test_verifier_rules.py b/tests/test_verifier_rules.py index a5e6d3b6..5c0761b7 100644 --- a/tests/test_verifier_rules.py +++ b/tests/test_verifier_rules.py @@ -79,9 +79,7 @@ def test_specification_mentions_atomic_request_id_check(): spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( encoding="utf-8" ) - assert "atomic" in spec.lower() - assert "request_id" in spec - assert "replay" in spec.lower() + assert "one atomic operation performed after both checks pass" in spec def test_specification_forbids_fallback_to_another_algorithm(): @@ -89,7 +87,7 @@ def test_specification_forbids_fallback_to_another_algorithm(): spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( encoding="utf-8" ) - assert "no fallback" in spec.lower() or "MUST NOT fall back" in spec + assert "with no fallback to another algorithm or key" in spec def test_specification_requires_constant_time_comparison(): @@ -97,7 +95,7 @@ def test_specification_requires_constant_time_comparison(): spec = (Path(__file__).resolve().parent.parent / "docs" / "spec" / "instrument" / "specification.md").read_text( encoding="utf-8" ) - assert "constant time" in spec.lower() or "constant-time" in spec.lower() + assert "verifiers MUST compare the decoded signature bytes in constant time" in spec def test_specification_isolates_failed_verification_from_state(): From df990f822a9518accc489c72aeb4827548934bf4 Mon Sep 17 00:00:00 2001 From: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:44:23 +0100 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20point=20rule=203=20at=20=C2=A710=20?= =?UTF-8?q?signature=20exemptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bar-capsule asked that rule 3 reference the exemptions stated in §10's opening sentence ("except the envelopes §10 exempts") instead of repeating the list. State the exemptions once in the opening (aligned with #339) and keep rule 3 as a pointer. Signed-off-by: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> --- docs/spec/instrument/specification.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/instrument/specification.md b/docs/spec/instrument/specification.md index a031315d..8d3f0d7a 100644 --- a/docs/spec/instrument/specification.md +++ b/docs/spec/instrument/specification.md @@ -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`. @@ -361,7 +361,7 @@ A signature over the §10 canonical input is REQUIRED in ACS-Core. The signature 2. **Constant-time comparison.** For MAC tags (HMAC-SHA256), verifiers MUST compare the decoded signature bytes in constant time to prevent timing-based recovery of the key. For asymmetric signatures (ECDSA, RSA-PSS, ML-DSA), verifiers MUST NOT compare base64-encoded wire text, and MUST NOT leak the key or secret material through an early exit or data-dependent timing. -3. **Missing signature.** An envelope that lacks a `signature` field MUST be rejected with `SIGNATURE_INVALID` (`-32004`, §17.1), except `system/ping` (§13) and error responses where no signing key can be resolved, such as the `SIGNATURE_INVALID` response for an unknown `key_id`. +3. **Missing signature.** An envelope that lacks a `signature` field MUST be rejected with `SIGNATURE_INVALID` (`-32004`, §17.1), except the envelopes §10 exempts. 4. **Non-negotiated algorithm.** The negotiated set for both directions (requests and responses) is the intersection of `ClientHello.signature_algorithms_supported` and `ServerHello.signature_algorithms_supported` (§4). If the ClientHello omits `signature_algorithms_supported`, the negotiated set contains only the ACS-Core baseline `HMAC-SHA256`. A Guardian MUST reject a request whose `signature.algorithm` is outside the negotiated set, returning `SIGNATURE_INVALID`, with no fallback to another algorithm or key. An Observed Agent MUST refuse a response whose `signature.algorithm` is outside the negotiated set locally, with no fallback. During the `handshake/hello` exchange itself, the Guardian verifies the ClientHello request against the algorithms it supports, and the Observed Agent verifies the ServerHello response against `ClientHello.signature_algorithms_supported` (or the baseline if omitted), because the intersection is not known until the ServerHello is processed.