From 65b74b886aadfc6610198a2a42a80ff7ce14df52 Mon Sep 17 00:00:00 2001 From: Jaime Fullaondo Date: Mon, 11 May 2026 17:58:07 +0200 Subject: [PATCH 1/2] Add Message Attestation extension for ServerToAgent messages Introduce an optional, opt-in, end-to-end integrity mechanism for every ServerToAgent message based on X.509 certificate chains. Trust is rooted in an operator-configured payload trust anchor that is distinct from the TLS CA pool, decoupling the OpAMP distribution server from the authoritative source of OpAMP messages. Wire-level changes (all Status: [Development]): * AgentCapabilities.RequiresPayloadTrustVerification = 0x00010000 * ServerCapabilities.OffersPayloadTrustVerification = 0x00000080 * ServerToAgent.trust_chain_response = 12 (new TrustChainResponse message) * ServerToAgent.signature = 13 The new "Message Attestation" section in specification.md covers the threat model, trust model, opt-in semantics, capability negotiation, connection-time handshake, in-session per-message verification, algorithm selection (any X.509-supported), certificate requirements, failure modes (all result in agent disconnect), and out-of-scope items. Strict opt-in: implementations that do not implement Message Attestation require no changes. Existing OpAMP deployments are unaffected until both sides opt in. Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Stanley Liu --- CHANGELOG.md | 15 ++ proto/opamp.proto | 51 +++++++ specification.md | 365 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 429 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35979e5..dc8840c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## Unreleased + +* Add `Message Attestation` section to specification.md describing an + optional, end-to-end integrity mechanism for `ServerToAgent` messages + based on X.509 certificate chains and a per-connection trust handshake. + Strict opt-in: existing OpAMP deployments are unaffected until both + Server and Agent opt in. +* Add `AgentCapabilities.RequiresPayloadTrustVerification = 0x00010000`. +* Add `ServerCapabilities.OffersPayloadTrustVerification = 0x00000080`. +* Add `ServerToAgent.trust_chain_response = 12` carrying the new + `TrustChainResponse` message. +* Add `ServerToAgent.signature = 13` carrying the per-message signature. +* Add new top-level `TrustChainResponse` message containing the + certificate chain and an optional error message. + ## v0.17.0 * Fix typos in TLS version comments by @Kielek in https://github.com/open-telemetry/opamp-spec/pull/316 diff --git a/proto/opamp.proto b/proto/opamp.proto index 3f84a18..bbfeadb 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -243,6 +243,23 @@ message ServerToAgent { // A custom message sent from the Server to an Agent. // Status: [Development] CustomMessage custom_message = 11; + + // Sent by the Server in its first ServerToAgent message in response to an + // Agent that has set the RequiresPayloadTrustVerification capability. + // Carries the signing certificate chain the Agent will use to verify + // subsequent ServerToAgent messages. If the Agent set + // RequiresPayloadTrustVerification but the first ServerToAgent does not + // include trust_chain_response, the Agent MUST terminate the connection. + // See the Message Attestation section of the specification. + // Status: [Development] + TrustChainResponse trust_chain_response = 12; + + // The signature of this ServerToAgent message. The exact bytes that are + // signed and the verification procedure are defined in the + // Message Attestation section of the specification. Set only after the + // payload trust verification handshake has completed successfully. + // Status: [Development] + bytes signature = 13; } enum ServerToAgentFlags { @@ -289,10 +306,38 @@ enum ServerCapabilities { // The Server can accept ConnectionSettingsRequest and respond with an offer. // Status: [Development] ServerCapabilities_AcceptsConnectionSettingsRequest = 0x00000040; + // The Server can respond to the payload trust verification handshake and + // sign every ServerToAgent message it sends after the handshake. See the + // Message Attestation section of the specification. + // Status: [Development] + ServerCapabilities_OffersPayloadTrustVerification = 0x00000080; // Add new capabilities here, continuing with the least significant unused bit. } +// TrustChainResponse carries the signing certificate chain used by the Server +// to sign subsequent ServerToAgent messages, as part of the payload trust +// verification handshake. See the Message Attestation section of the +// specification. +// Status: [Development] +message TrustChainResponse { + message Certificate { + // The certificate in DER format. + bytes der_data = 1; + } + + // The certificate chain, ordered from the first intermediate certificate + // down to the signing leaf certificate. The root certificate is excluded; + // the Agent already possesses the root as its pre-configured payload + // trust anchor. + repeated Certificate certificate_chain = 1; + + // Human-readable error message indicating why the Server could not + // satisfy the trust chain request. If error_message is set, the Agent + // MUST terminate the connection. + string error_message = 2; +} + // The OpAMPConnectionSettings message is a collection of fields which comprise an // offer from the Server to the Agent to use the specified settings for OpAMP // connection. @@ -781,6 +826,12 @@ enum AgentCapabilities { // The agent will report ConnectionSettingsOffers status via AgentToServer.connection_settings_status field. // Status: [Development] AgentCapabilities_ReportsConnectionSettingsStatus = 0x00008000; + // The Agent requires the payload trust verification handshake on connection + // and signature verification on every subsequent ServerToAgent message. + // If the Server does not offer this capability, the Agent MUST terminate + // the connection. See the Message Attestation section of the specification. + // Status: [Development] + AgentCapabilities_RequiresPayloadTrustVerification = 0x00010000; // Add new capabilities here, continuing with the least significant unused bit. } diff --git a/specification.md b/specification.md index 9e0443a..12c5efd 100644 --- a/specification.md +++ b/specification.md @@ -54,6 +54,8 @@ Status: [Beta] - [ServerToAgent.command](#servertoagentcommand) - [ServerToAgent.custom_capabilities](#servertoagentcustom_capabilities) - [ServerToAgent.custom_message](#servertoagentcustom_message) + - [ServerToAgent.trust_chain_response](#servertoagenttrust_chain_response) + - [ServerToAgent.signature](#servertoagentsignature) + [ServerErrorResponse Message](#servererrorresponse-message) - [ServerErrorResponse.type](#servererrorresponsetype) - [ServerErrorResponse.error_message](#servererrorresponseerror_message) @@ -225,6 +227,20 @@ Status: [Beta] * [Configuration Restrictions](#configuration-restrictions) * [Opt-in Remote Configuration](#opt-in-remote-configuration) * [Code Signing](#code-signing) +- [Message Attestation](#message-attestation) + * [Motivation and Threat Model](#motivation-and-threat-model) + * [Trust Model](#trust-model) + * [Opt-in and Backwards Compatibility](#opt-in-and-backwards-compatibility) + * [Capability Negotiation](#capability-negotiation) + * [Connection-Time Handshake](#connection-time-handshake) + + [TrustChainResponse Message](#trustchainresponse-message) + - [TrustChainResponse.certificate_chain](#trustchainresponsecertificate_chain) + - [TrustChainResponse.error_message](#trustchainresponseerror_message) + * [In-Session Signature Verification](#in-session-signature-verification) + * [Algorithm](#algorithm) + * [Certificate Requirements](#certificate-requirements) + * [Failure Modes](#failure-modes) + * [Out of Scope](#out-of-scope) - [Interoperability](#interoperability) * [Interoperability of Partial Implementations](#interoperability-of-partial-implementations) * [Interoperability of Future Capabilities](#interoperability-of-future-capabilities) @@ -656,6 +672,12 @@ enum AgentCapabilities { // The agent will report ConnectionSettingsOffers status via AgentToServer.connection_settings_status field. // Status: [Development] ReportsConnectionSettingsStatus = 0x00008000; + // The Agent requires the payload trust verification handshake on connection + // and signature verification on every subsequent ServerToAgent message. + // If the Server does not offer this capability, the Agent MUST terminate + // the connection. See the [Message Attestation](#message-attestation) section. + // Status: [Development] + RequiresPayloadTrustVerification = 0x00010000; // Add new capabilities here, continuing with the least significant unused bit. } @@ -917,6 +939,11 @@ enum ServerCapabilities { // The Server can accept ConnectionSettingsRequest and respond with an offer. // Status: [Development] AcceptsConnectionSettingsRequest = 0x00000040; + // The Server can respond to the payload trust verification handshake and + // sign every ServerToAgent message it sends after the handshake. + // See the [Message Attestation](#message-attestation) section. + // Status: [Development] + OffersPayloadTrustVerification = 0x00000080; // Add new capabilities here, continuing with the least significant unused bit. } @@ -962,6 +989,32 @@ A custom message sent from the Server to an Agent. See [CustomMessage](#custommessage) message for details. +##### ServerToAgent.trust_chain_response + +Status: [Development] + +Sent by the Server in its first `ServerToAgent` message in response to an +Agent that has set the +[`RequiresPayloadTrustVerification`](#agenttoservercapabilities) capability. The +Agent uses the contained certificate chain to verify the signature on +every subsequent `ServerToAgent` message. + +See the [Message Attestation](#message-attestation) section for the full +handshake and verification procedure, and the +[`TrustChainResponse`](#trustchainresponse-message) message for the field +layout. + +##### ServerToAgent.signature + +Status: [Development] + +The signature of this `ServerToAgent` message. Set by the Server only +after a successful payload trust verification handshake, and verified by +the Agent against the leaf certificate established by that handshake. + +The exact bytes that are signed and the verification procedure are +defined in the [Message Attestation](#message-attestation) section. + #### ServerErrorResponse Message The message has the following structure: @@ -3541,8 +3594,11 @@ Agent by default. The capabilities should be opt-in by the user. ### Code Signing Any executable code that is part of a package should be signed -to prevent a compromised Server from delivering malicious code to the Agent. We -recommend the following: +to prevent a compromised Server from delivering malicious code to the Agent. +For end-to-end integrity of OpAMP messages themselves (including remote +configuration offers, package offers, and Server-issued commands), see the +[Message Attestation](#message-attestation) section. +We recommend the following: * Any downloadable executable code (e.g. executable packages) need to be code-signed. The actual code-signing and verification mechanism is @@ -3561,6 +3617,311 @@ recommend the following: prevent the code from accessing sensitive files or perform high privilege operations. The Agent should not run downloaded code as root user. +## Message Attestation + +**Status: [Development]** + +This section specifies an optional, end-to-end integrity mechanism for +`ServerToAgent` messages based on X.509 certificate chains. When both the +Server and the Agent opt in, every `ServerToAgent` message sent after the +initial handshake carries a signature that the Agent verifies against a +pre-configured trust anchor (a CA certificate) that is **distinct** from +the TLS certificate authority used to establish the transport. + +The mechanism allows OpAMP deployments to separate the _distribution +server_ from the _authoritative source of OpAMP messages_. A compromised +distribution server cannot, in this model, push arbitrary configuration +or commands to an Agent, because every message must be signed by a key +whose trust chain validates against the Agent's pre-configured root. + +### Motivation and Threat Model + +TLS provides transport-level security between Server and Agent. It does +not provide end-to-end integrity from the authoritative source of OpAMP +messages: if TLS is terminated at a third-party load balancer, or if a +managed OpAMP server is operated by a vendor, the distribution server +can be a single point of control over the fleet. Even when TLS is not +terminated by a third party, a software exploit of the distribution +server (for example, a remote code execution vulnerability) could allow +an attacker to take over the entire fleet. + +Without message-level signatures, the following attack vectors are not +mitigated by the protocol itself: + +* A compromised distribution server may forge or modify + [`ServerToAgent.remote_config`](#servertoagentremote_config), + [`ServerToAgent.connection_settings`](#servertoagentconnection_settings), + [`ServerToAgent.packages_available`](#servertoagentpackages_available), + [`ServerToAgent.command`](#servertoagentcommand), + [`ServerToAgent.agent_identification`](#servertoagentagent_identification), + or any other field, even though such messages did not originate from + the intended authoritative source. + +* An internal attacker who can access the distribution server's stored + state, but not the signing keys, can still alter OpAMP messages in + flight. + +Message Attestation does **not** address: + +* Encryption of `ServerToAgent` or `AgentToServer` message contents + (the transport-level encryption provided by TLS remains the mechanism + for confidentiality). +* Authentication of `AgentToServer` messages (the Server's ability to + verify the authenticity of Agent messages is out of scope for this + section; see + [opamp-spec issue #20](https://github.com/open-telemetry/opamp-spec/issues/20)). + +### Trust Model + +The Agent is pre-configured with a single root CA certificate, referred +to in this section as the _payload trust anchor_. This certificate is +operator-managed and is supplied to the Agent through +implementation-specific configuration (for example, a file path in the +Agent's configuration). The payload trust anchor MUST NOT be the same +certificate as the TLS root the Agent uses to validate the transport — +using the same root would collapse two separate trust domains into one +and eliminate the security benefit. + +The Server is independently configured with a signing key and its +corresponding certificate chain that validates back to the payload trust +anchor. + +The payload trust anchor is NEVER pushed by the Server. In particular, +no field of any `ServerToAgent` message — including +[`OpAMPConnectionSettings`](#opampconnectionsettings) — may be used to +update or replace the Agent's payload trust anchor. This is a deliberate +constraint to prevent a compromised Server from rotating the Agent onto +an attacker-controlled trust anchor. + +### Opt-in and Backwards Compatibility + +Message Attestation is a strict opt-in feature. + +**Implementations that do not implement Message Attestation are not +required to change. Existing OpAMP deployments are unaffected by this +specification until both sides opt in.** + +* Agents opt in by configuring a payload trust anchor at startup. + Opting in causes the Agent to set the + [`RequiresPayloadTrustVerification`](#agenttoservercapabilities) capability + bit in its first `AgentToServer.capabilities`. +* Servers opt in by configuring a signing key and certificate chain. + Opting in causes the Server to set the + [`OffersPayloadTrustVerification`](#servertoagentcapabilities) capability + bit in its `ServerToAgent.capabilities`. + +The new capability bits are additions to a 64-bit bitmask. +Implementations that do not recognise the new bits will simply not match +them and will behave exactly as today. The new `ServerToAgent` fields +(`trust_chain_response` and `signature`) are proto3 scalar/message +additions and are ignored by implementations that predate them. + +A Server may advertise `OffersPayloadTrustVerification` and only sign +`ServerToAgent` messages on connections from Agents that have set +`RequiresPayloadTrustVerification` — there is no per-message cost for +advertising the capability to non-opted-in Agents. + +### Capability Negotiation + +| Agent `Requires` | Server `Offers` | Behaviour | +| --- | --- | --- | +| No | No | Plain OpAMP. Today's behaviour. | +| No | Yes | Plain OpAMP. The Server is capable of signing but the Agent has not opted in, so no `trust_chain_response` is sent and no signatures are emitted. | +| Yes | No | The Server's first `ServerToAgent` lacks `trust_chain_response`. The Agent MUST terminate the connection. | +| Yes | Yes | Handshake on the first `ServerToAgent`; per-message signatures thereafter. Specified in the remainder of this section. | + +### Connection-Time Handshake + +When the Agent has set `RequiresPayloadTrustVerification` and the Server +has set `OffersPayloadTrustVerification`, the first `ServerToAgent` +message exchanged on the connection MUST carry a +[`trust_chain_response`](#servertoagenttrust_chain_response). + +1. The Agent's first `AgentToServer` message sets the + `RequiresPayloadTrustVerification` bit in `capabilities`. + +2. The Server's first `ServerToAgent` message: + * MUST set `trust_chain_response.certificate_chain` to the ordered + list of certificates from the first intermediate down to the + signing leaf certificate. The root certificate (the Agent's + pre-configured payload trust anchor) MUST NOT be included in this + chain. + * MAY set `trust_chain_response.error_message` if the Server cannot + satisfy the trust chain request (for example, because its signing + key is unavailable). When `error_message` is non-empty, + `certificate_chain` SHOULD be empty. + * MAY omit the [`signature`](#servertoagentsignature) field on this + first message. Trust on the first message is established by chain + validation against the pre-configured trust anchor, not by a + self-signature. Every subsequent `ServerToAgent` MUST be signed. + +3. The Agent receives the Server's first `ServerToAgent` and: + * If `trust_chain_response` is unset, the Agent MUST terminate the + connection. + * If `trust_chain_response.error_message` is non-empty, the Agent + MUST terminate the connection. + * Otherwise the Agent performs standard X.509 path validation of + `certificate_chain` using the pre-configured payload trust anchor + as the trust root. If validation fails — for any reason, including + expired leaf, expired intermediate, unknown issuer, missing + `id-kp-codeSigning` Extended Key Usage on the leaf, or revocation + — the Agent MUST terminate the connection. + * On successful validation, the Agent stores the validated leaf + certificate (or its public key) for the duration of the connection + and uses it to verify every subsequent `ServerToAgent` message. + +#### TrustChainResponse Message + +```protobuf +message TrustChainResponse { + message Certificate { + // The certificate in DER format. + bytes der_data = 1; + } + + // The certificate chain, ordered from the first intermediate + // certificate down to the signing leaf certificate. The root + // certificate is excluded; the Agent already possesses the root as + // its pre-configured payload trust anchor. + repeated Certificate certificate_chain = 1; + + // Human-readable error message indicating why the Server could not + // satisfy the trust chain request. If error_message is non-empty, + // the Agent MUST terminate the connection. + string error_message = 2; +} +``` + +##### TrustChainResponse.certificate_chain + +Ordered list of certificates from the first intermediate down to the +signing leaf certificate. The root certificate is excluded. Each entry +is a `Certificate` message whose `der_data` field holds the DER-encoded +certificate. + +##### TrustChainResponse.error_message + +Human-readable error description set by the Server when it cannot +satisfy the trust chain request. When non-empty, `certificate_chain` +SHOULD be empty and the Agent MUST terminate the connection. + +### In-Session Signature Verification + +For every `ServerToAgent` message after the first, the Server MUST set +the `signature` field to a signature over the message bytes, computed as +follows: + +1. Construct the `ServerToAgent` message with all required fields set + except `signature`, which is left empty (zero-length). +2. Serialise the message using deterministic Protocol Buffers encoding + (for example, Go's `proto.MarshalOptions{Deterministic: true}` or + the equivalent in other implementations). +3. Sign the resulting byte string with the Server's signing private + key, using the signature algorithm declared by the leaf + certificate's `signatureAlgorithm` field. +4. Set `signature` to the resulting signature bytes. + +The Agent verifies a received `ServerToAgent` message as follows: + +1. Extract the `signature` field. If `signature` is empty or absent on + any message after the handshake, the Agent MUST terminate the + connection. +2. Construct a copy of the received message with `signature` cleared + (set to empty bytes). +3. Serialise the copy using deterministic Protocol Buffers encoding. +4. Verify the extracted `signature` against the resulting byte string + using the public key of the leaf certificate established during the + handshake, and the signature algorithm declared by the leaf + certificate's `signatureAlgorithm`. +5. If verification fails, the Agent MUST terminate the connection. + +Implementations MUST use deterministic Protocol Buffers encoding for +both signing and verification. Cross-language implementations must +produce byte-identical canonical serialisations for the signed payload +to interoperate. + +### Algorithm + +The signing algorithm is determined by the leaf certificate's +`signatureAlgorithm` field. The OpAMP protocol does not negotiate +algorithms. + +Implementations SHOULD support, at minimum, the following algorithms: + +* ECDSA P-256 with SHA-256 +* ECDSA P-384 with SHA-384 +* RSA-2048 or larger with PKCS#1 v1.5 and SHA-256 +* Ed25519 + +These algorithms are covered by the default X.509 stacks of Go +(`crypto/x509`), Java (`java.security`), and Python (`cryptography`). + +Future algorithm additions require no change to the OpAMP protocol; new +algorithms are signalled by the certificate and supported by stacks as +they evolve. + +### Certificate Requirements + +The signing leaf certificate MUST: + +* Include the Extended Key Usage extension with `id-kp-codeSigning` + (`1.3.6.1.5.5.7.3.3`) in its list of allowed usages. This prevents a + certificate intended for TLS server authentication from being used + to sign OpAMP messages. +* Be within its validity window + (`notBefore <= currentTime < notAfter`) at every verification. + +The certificate chain (intermediates plus leaf) MUST chain to the +pre-configured payload trust anchor. The trust anchor itself is +supplied out-of-band and MUST NOT be included in the +`certificate_chain` field of `trust_chain_response`. + +Revocation checking is RECOMMENDED. Implementations SHOULD use the +revocation-checking facilities of their X.509 library (CRL distribution +points, OCSP) during chain validation. Operators MAY rely on +short-lived certificates as an alternative to active revocation. + +### Failure Modes + +Every failure listed below MUST cause the Agent to terminate the OpAMP +connection. Recovery is by reconnection; the Server presents a +(potentially rotated) chain on the new handshake. + +| Failure | When detected | +| --- | --- | +| Agent set `RequiresPayloadTrustVerification` but the Server did not set `OffersPayloadTrustVerification`. | First `ServerToAgent` (capability bits visible at that time). | +| Server's first `ServerToAgent` does not include `trust_chain_response`. | First `ServerToAgent`. | +| `trust_chain_response.error_message` is non-empty. | First `ServerToAgent`. | +| Certificate chain fails X.509 path validation (expired certificate, unknown issuer, missing `id-kp-codeSigning` EKU, revoked certificate, etc.). | First `ServerToAgent`. | +| In-session `ServerToAgent` message lacks `signature`. | Any subsequent `ServerToAgent`. | +| In-session `signature` does not verify against the stored leaf certificate. | Any subsequent `ServerToAgent`. | +| Stored leaf certificate's validity window has expired since the handshake. | The next verification after expiry. | + +### Out of Scope + +The following are explicitly out of scope for this version of Message +Attestation. They MAY be revisited in future versions of the +specification. + +* **Encryption of message contents.** TLS continues to provide + transport-level confidentiality. Message Attestation adds integrity, + not confidentiality. +* **Authentication of `AgentToServer` messages.** Verifying the + authenticity of Agent-originated messages is tracked separately in + [opamp-spec issue #20](https://github.com/open-telemetry/opamp-spec/issues/20). +* **Trust anchor distribution by the Server.** The payload trust anchor + is operator-managed and MUST NOT be modified by any field of any + `ServerToAgent` message. +* **Algorithm negotiation.** The certificate's `signatureAlgorithm` is + authoritative. Future algorithm support is added by certificate + issuers and X.509 stacks, not by changes to OpAMP. +* **Per-message-type opt-out (signing allowlist).** Mechanisms by which + an Agent might accept some `ServerToAgent` message types unsigned — + for example, to allow a third-party fleet manager to push low-risk + read-only telemetry settings while still requiring authoritative + signatures for configuration or command messages — are deferred to a + follow-up specification. + ## Interoperability ### Interoperability of Partial Implementations From 1f75ca1f63a0006e627bc8f23c13a9279a1232ac Mon Sep 17 00:00:00 2001 From: Jaime Fullaondo Date: Wed, 20 May 2026 00:31:43 +0200 Subject: [PATCH 2/2] Pivot Message Attestation to SignedServerToAgent envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Response to PR #333 review feedback (discussion_r3235566207). The previous design signed an inline ServerToAgent.signature field by clearing the field, deterministically re-marshalling, and signing the result. That requires byte-identical "deterministic" output across every protobuf implementation and version, which proto explicitly does not guarantee (https://protobuf.dev/programming-guides/serialization-not-canonical/). This commit moves the signature and trust-chain delivery onto a new top-level envelope message: * SignedServerToAgent { bytes payload = 1; bytes signature = 2; TrustChainResponse trust_chain_response = 3; } * `payload` carries the marshalled bytes of an inner ServerToAgent; `signature` is a detached signature over those exact wire bytes (the Agent verifies without re-marshalling). The envelope is sent only when both peers have negotiated Message Attestation (RequiresPayloadTrustVerification + OffersPayloadTrustVerification). For all other connections the wire format is byte-identical to upstream OpAMP — ServerToAgent is untouched, and implementations that don't opt in see no wire change. Removes the trust_chain_response (field 12) and signature (field 13) inline fields added to ServerToAgent in the previous commit; both field numbers and names are now reserved on ServerToAgent. Spec narrative updated: rewrites Connection-Time Handshake and In-Session Signature Verification subsections; adds a new SignedServerToAgent Message subsection; adds a rationale paragraph citing the protobuf non-canonicality guidance. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 18 +++- proto/opamp.proto | 61 +++++++++--- specification.md | 242 ++++++++++++++++++++++++++++------------------ 3 files changed, 206 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8840c..a8b3d1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,23 @@ * Add `Message Attestation` section to specification.md describing an optional, end-to-end integrity mechanism for `ServerToAgent` messages based on X.509 certificate chains and a per-connection trust handshake. - Strict opt-in: existing OpAMP deployments are unaffected until both - Server and Agent opt in. + Strict opt-in at the wire level: existing OpAMP deployments are + unaffected until both Server and Agent opt in. * Add `AgentCapabilities.RequiresPayloadTrustVerification = 0x00010000`. * Add `ServerCapabilities.OffersPayloadTrustVerification = 0x00000080`. -* Add `ServerToAgent.trust_chain_response = 12` carrying the new - `TrustChainResponse` message. -* Add `ServerToAgent.signature = 13` carrying the per-message signature. +* Add new top-level `SignedServerToAgent` envelope message containing + the marshalled `ServerToAgent` `payload`, a detached `signature` over + the payload bytes, and (on the first message of a connection) the + `trust_chain_response` carrying the signing certificate chain. The + envelope is used only when payload trust verification has been + negotiated; otherwise the Server keeps sending plain `ServerToAgent` + messages on the wire, byte-identical to upstream OpAMP. * Add new top-level `TrustChainResponse` message containing the certificate chain and an optional error message. +* Reserve field numbers 12 and 13 on `ServerToAgent` (briefly used by + an earlier draft for inline trust-chain and signature fields; that + draft was superseded by the `SignedServerToAgent` envelope so the + numbers can never be reused). ## v0.17.0 diff --git a/proto/opamp.proto b/proto/opamp.proto index bbfeadb..a41625c 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -244,22 +244,14 @@ message ServerToAgent { // Status: [Development] CustomMessage custom_message = 11; - // Sent by the Server in its first ServerToAgent message in response to an - // Agent that has set the RequiresPayloadTrustVerification capability. - // Carries the signing certificate chain the Agent will use to verify - // subsequent ServerToAgent messages. If the Agent set - // RequiresPayloadTrustVerification but the first ServerToAgent does not - // include trust_chain_response, the Agent MUST terminate the connection. - // See the Message Attestation section of the specification. - // Status: [Development] - TrustChainResponse trust_chain_response = 12; - - // The signature of this ServerToAgent message. The exact bytes that are - // signed and the verification procedure are defined in the - // Message Attestation section of the specification. Set only after the - // payload trust verification handshake has completed successfully. - // Status: [Development] - bytes signature = 13; + // Field numbers 12 and 13 were briefly assigned to inline payload + // trust verification metadata (trust_chain_response and signature) + // in an earlier draft of the Message Attestation spec. The design + // moved that metadata onto a separate envelope message + // (SignedServerToAgent) to enable detached signing. The field + // numbers are reserved here to prevent accidental reuse. + reserved 12, 13; + reserved "trust_chain_response", "signature"; } enum ServerToAgentFlags { @@ -338,6 +330,43 @@ message TrustChainResponse { string error_message = 2; } +// SignedServerToAgent wraps a ServerToAgent message when the payload trust +// verification handshake has been negotiated between Server and Agent. When +// both AgentCapabilities_RequiresPayloadTrustVerification (set by the Agent) +// and ServerCapabilities_OffersPayloadTrustVerification (set by the Server) +// are advertised, every Server-to-Agent message on the connection is wrapped +// in SignedServerToAgent. +// +// The signature is computed and verified over the bytes of the payload field +// exactly as they appear on the wire (a "detached" signature). This avoids +// any dependency on canonical protobuf encoding, which is not guaranteed +// across protobuf library versions, schema changes, or build flags. +// +// See the Message Attestation section of the specification. +// Status: [Development] +message SignedServerToAgent { + // Serialised bytes of a ServerToAgent message. The Agent verifies the + // detached signature over these exact bytes, without re-marshalling, + // and then unmarshals them into a ServerToAgent for normal processing. + bytes payload = 1; + + // Detached signature over the bytes of the payload field. MAY be empty + // on the first SignedServerToAgent of a connection: trust on the first + // message is established by validating the certificate chain carried in + // trust_chain_response against the Agent's pre-configured payload trust + // anchor. MUST be present and verifiable on every subsequent + // SignedServerToAgent. + bytes signature = 2; + + // Sent only in the first SignedServerToAgent on a connection. Carries + // the signing certificate chain the Agent will use to verify signatures + // on subsequent messages. If the Agent set + // RequiresPayloadTrustVerification but the first SignedServerToAgent + // does not include a usable trust_chain_response, the Agent MUST + // terminate the connection. + TrustChainResponse trust_chain_response = 3; +} + // The OpAMPConnectionSettings message is a collection of fields which comprise an // offer from the Server to the Agent to use the specified settings for OpAMP // connection. diff --git a/specification.md b/specification.md index 12c5efd..0deef1b 100644 --- a/specification.md +++ b/specification.md @@ -54,8 +54,6 @@ Status: [Beta] - [ServerToAgent.command](#servertoagentcommand) - [ServerToAgent.custom_capabilities](#servertoagentcustom_capabilities) - [ServerToAgent.custom_message](#servertoagentcustom_message) - - [ServerToAgent.trust_chain_response](#servertoagenttrust_chain_response) - - [ServerToAgent.signature](#servertoagentsignature) + [ServerErrorResponse Message](#servererrorresponse-message) - [ServerErrorResponse.type](#servererrorresponsetype) - [ServerErrorResponse.error_message](#servererrorresponseerror_message) @@ -233,6 +231,10 @@ Status: [Beta] * [Opt-in and Backwards Compatibility](#opt-in-and-backwards-compatibility) * [Capability Negotiation](#capability-negotiation) * [Connection-Time Handshake](#connection-time-handshake) + + [SignedServerToAgent Message](#signedservertoagent-message) + - [SignedServerToAgent.payload](#signedservertoagentpayload) + - [SignedServerToAgent.signature](#signedservertoagentsignature) + - [SignedServerToAgent.trust_chain_response](#signedservertoagenttrust_chain_response) + [TrustChainResponse Message](#trustchainresponse-message) - [TrustChainResponse.certificate_chain](#trustchainresponsecertificate_chain) - [TrustChainResponse.error_message](#trustchainresponseerror_message) @@ -989,32 +991,6 @@ A custom message sent from the Server to an Agent. See [CustomMessage](#custommessage) message for details. -##### ServerToAgent.trust_chain_response - -Status: [Development] - -Sent by the Server in its first `ServerToAgent` message in response to an -Agent that has set the -[`RequiresPayloadTrustVerification`](#agenttoservercapabilities) capability. The -Agent uses the contained certificate chain to verify the signature on -every subsequent `ServerToAgent` message. - -See the [Message Attestation](#message-attestation) section for the full -handshake and verification procedure, and the -[`TrustChainResponse`](#trustchainresponse-message) message for the field -layout. - -##### ServerToAgent.signature - -Status: [Development] - -The signature of this `ServerToAgent` message. Set by the Server only -after a successful payload trust verification handshake, and verified by -the Agent against the leaf certificate established by that handshake. - -The exact bytes that are signed and the verification procedure are -defined in the [Message Attestation](#message-attestation) section. - #### ServerErrorResponse Message The message has the following structure: @@ -3712,52 +3688,69 @@ specification until both sides opt in.** The new capability bits are additions to a 64-bit bitmask. Implementations that do not recognise the new bits will simply not match -them and will behave exactly as today. The new `ServerToAgent` fields -(`trust_chain_response` and `signature`) are proto3 scalar/message -additions and are ignored by implementations that predate them. +them and will behave exactly as today. -A Server may advertise `OffersPayloadTrustVerification` and only sign -`ServerToAgent` messages on connections from Agents that have set -`RequiresPayloadTrustVerification` — there is no per-message cost for -advertising the capability to non-opted-in Agents. +The `SignedServerToAgent` envelope is sent **only** when the negotiation +succeeds. For connections where Message Attestation is not negotiated, +the wire format is byte-identical to upstream OpAMP — the Server keeps +sending plain `ServerToAgent` messages, and the Agent keeps parsing +them as such. Implementations that do not implement Message Attestation +therefore see no wire-format change at all. + +A Server may advertise `OffersPayloadTrustVerification` and only wrap +its outbound messages in `SignedServerToAgent` on connections from +Agents that have set `RequiresPayloadTrustVerification` — there is no +per-message cost for advertising the capability to non-opted-in Agents. ### Capability Negotiation | Agent `Requires` | Server `Offers` | Behaviour | | --- | --- | --- | -| No | No | Plain OpAMP. Today's behaviour. | -| No | Yes | Plain OpAMP. The Server is capable of signing but the Agent has not opted in, so no `trust_chain_response` is sent and no signatures are emitted. | -| Yes | No | The Server's first `ServerToAgent` lacks `trust_chain_response`. The Agent MUST terminate the connection. | -| Yes | Yes | Handshake on the first `ServerToAgent`; per-message signatures thereafter. Specified in the remainder of this section. | +| No | No | Plain OpAMP. The Server sends `ServerToAgent` messages on the wire. Today's behaviour. | +| No | Yes | Plain OpAMP. The Server is capable of signing but the Agent has not opted in, so the Server sends `ServerToAgent` messages on the wire (not `SignedServerToAgent`). | +| Yes | No | The Server does not send `SignedServerToAgent`. The Agent receives a plain `ServerToAgent` where it expected a `SignedServerToAgent` envelope, and MUST terminate the connection. | +| Yes | Yes | Every Server-to-Agent message on the connection is wrapped in `SignedServerToAgent`. Handshake on the first message (carries `trust_chain_response`); per-message detached signatures thereafter. Specified in the remainder of this section. | ### Connection-Time Handshake When the Agent has set `RequiresPayloadTrustVerification` and the Server -has set `OffersPayloadTrustVerification`, the first `ServerToAgent` -message exchanged on the connection MUST carry a -[`trust_chain_response`](#servertoagenttrust_chain_response). +has set `OffersPayloadTrustVerification`, every Server-to-Agent message +on the connection is wrapped in a `SignedServerToAgent` envelope. The +first such envelope carries the signing certificate chain in +`trust_chain_response`. 1. The Agent's first `AgentToServer` message sets the `RequiresPayloadTrustVerification` bit in `capabilities`. -2. The Server's first `ServerToAgent` message: - * MUST set `trust_chain_response.certificate_chain` to the ordered - list of certificates from the first intermediate down to the - signing leaf certificate. The root certificate (the Agent's - pre-configured payload trust anchor) MUST NOT be included in this - chain. +2. The Server, on receiving the Agent's first message and recognising + the capability: + * Sends its first `SignedServerToAgent` containing + `trust_chain_response.certificate_chain` ordered from the first + intermediate down to the signing leaf certificate. The root + certificate (the Agent's pre-configured payload trust anchor) + MUST NOT be included in this chain. * MAY set `trust_chain_response.error_message` if the Server cannot satisfy the trust chain request (for example, because its signing key is unavailable). When `error_message` is non-empty, `certificate_chain` SHOULD be empty. - * MAY omit the [`signature`](#servertoagentsignature) field on this - first message. Trust on the first message is established by chain - validation against the pre-configured trust anchor, not by a - self-signature. Every subsequent `ServerToAgent` MUST be signed. - -3. The Agent receives the Server's first `ServerToAgent` and: - * If `trust_chain_response` is unset, the Agent MUST terminate the - connection. + * MAY leave `signature` empty on this first envelope. Trust on the + first message is established by chain validation against the + pre-configured trust anchor, not by a self-signature. Every + subsequent `SignedServerToAgent` MUST carry a valid signature. + * Sets `payload` to the marshalled bytes of a `ServerToAgent` + message containing whatever the Server would have sent had + signing not been negotiated (for example, an empty `ServerToAgent` + acknowledging the Agent's status report, or a `ServerToAgent` + carrying initial `remote_config`). + +3. The Agent receives the Server's first envelope and: + * Parses the bytes on the wire as `SignedServerToAgent`. If the + bytes cannot be parsed as `SignedServerToAgent`, or + `trust_chain_response` is unset, the Agent MUST terminate the + connection. (This is also how the Agent detects a Server that + does not support Message Attestation: such a Server would send a + plain `ServerToAgent`, which does not parse as + `SignedServerToAgent`.) * If `trust_chain_response.error_message` is non-empty, the Agent MUST terminate the connection. * Otherwise the Agent performs standard X.509 path validation of @@ -3767,8 +3760,48 @@ message exchanged on the connection MUST carry a `id-kp-codeSigning` Extended Key Usage on the leaf, or revocation — the Agent MUST terminate the connection. * On successful validation, the Agent stores the validated leaf - certificate (or its public key) for the duration of the connection - and uses it to verify every subsequent `ServerToAgent` message. + certificate (or its public key) for the duration of the + connection and uses it to verify every subsequent + `SignedServerToAgent`. + * The Agent then unmarshals the `payload` bytes into a + `ServerToAgent` and processes it normally. + +#### SignedServerToAgent Message + +```protobuf +message SignedServerToAgent { + // Serialised bytes of a ServerToAgent message. + bytes payload = 1; + + // Detached signature over the bytes of the payload field. + bytes signature = 2; + + // Sent only in the first SignedServerToAgent on a connection. + TrustChainResponse trust_chain_response = 3; +} +``` + +##### SignedServerToAgent.payload + +Marshalled bytes of an inner `ServerToAgent` message. The Server +marshals the inner `ServerToAgent` once and places the resulting bytes +here. The signature in `signature` covers these exact bytes; the Agent +verifies the signature without re-marshalling, and then unmarshals +these bytes into a `ServerToAgent` for normal processing. + +##### SignedServerToAgent.signature + +Detached signature over the bytes of `payload`. MAY be empty on the +first `SignedServerToAgent` of a connection — chain validation against +the pre-configured trust anchor establishes initial trust. MUST be +present and verifiable on every subsequent message. + +##### SignedServerToAgent.trust_chain_response + +Sent only in the first `SignedServerToAgent` on a connection. Carries +the signing certificate chain the Agent will use to verify signatures +on subsequent messages. See +[TrustChainResponse Message](#trustchainresponse-message). #### TrustChainResponse Message @@ -3807,38 +3840,59 @@ SHOULD be empty and the Agent MUST terminate the connection. ### In-Session Signature Verification -For every `ServerToAgent` message after the first, the Server MUST set -the `signature` field to a signature over the message bytes, computed as -follows: - -1. Construct the `ServerToAgent` message with all required fields set - except `signature`, which is left empty (zero-length). -2. Serialise the message using deterministic Protocol Buffers encoding - (for example, Go's `proto.MarshalOptions{Deterministic: true}` or - the equivalent in other implementations). -3. Sign the resulting byte string with the Server's signing private - key, using the signature algorithm declared by the leaf +Signatures are computed and verified **over the bytes of the inner +`ServerToAgent` exactly as they appear on the wire in +`SignedServerToAgent.payload`** — a "detached" signature scheme. The +Server marshals each inner `ServerToAgent` once, signs those bytes, +and places them into `payload`; the Agent verifies the signature over +the received `payload` bytes without re-marshalling. + +> **Why detached signing?** Protocol Buffers does not guarantee a +> canonical wire-format encoding, even with deterministic-output +> options enabled. The serializer can produce different output across +> protobuf library versions, schema changes, and build flags (see the +> upstream guidance at +> ). +> Any signature scheme that requires the receiver to re-marshal a +> parsed message and reproduce the signed bytes would therefore be +> fragile across implementations and versions. Detached signing over +> the wire bytes side-steps the problem entirely: the signed bytes are +> the wire bytes, and they survive any number of round-trips through +> different protobuf libraries. + +The Server produces a `SignedServerToAgent` as follows: + +1. Construct the inner `ServerToAgent` message normally. +2. Marshal the inner message to bytes using any conformant Protocol + Buffers encoder. (No special "deterministic" option is required; + the only bytes that matter are the ones placed on the wire.) +3. Compute a signature over those bytes using the Server's signing + private key and the signature algorithm declared by the leaf certificate's `signatureAlgorithm` field. -4. Set `signature` to the resulting signature bytes. - -The Agent verifies a received `ServerToAgent` message as follows: - -1. Extract the `signature` field. If `signature` is empty or absent on - any message after the handshake, the Agent MUST terminate the +4. Construct the outer `SignedServerToAgent` with `payload` set to the + marshalled bytes from step 2 and `signature` set to the signature + from step 3. On the first message, also set `trust_chain_response`. +5. Marshal and send the `SignedServerToAgent`. + +The Agent verifies a received `SignedServerToAgent` (after the first) +as follows: + +1. Parse the wire bytes as a `SignedServerToAgent`. Retain the + `payload` field's raw bytes — these are the bytes the signature + covers. +2. If `signature` is empty or absent, the Agent MUST terminate the connection. -2. Construct a copy of the received message with `signature` cleared - (set to empty bytes). -3. Serialise the copy using deterministic Protocol Buffers encoding. -4. Verify the extracted `signature` against the resulting byte string - using the public key of the leaf certificate established during the - handshake, and the signature algorithm declared by the leaf - certificate's `signatureAlgorithm`. -5. If verification fails, the Agent MUST terminate the connection. - -Implementations MUST use deterministic Protocol Buffers encoding for -both signing and verification. Cross-language implementations must -produce byte-identical canonical serialisations for the signed payload -to interoperate. +3. Verify `signature` over the `payload` bytes using the public key of + the leaf certificate established during the handshake, and the + signature algorithm declared by the leaf certificate's + `signatureAlgorithm`. +4. If verification fails, the Agent MUST terminate the connection. +5. On success, unmarshal the `payload` bytes into a `ServerToAgent` + and process it normally. + +Because the signature is detached, the Agent never needs to re-marshal +the inner `ServerToAgent`. This eliminates any dependency on canonical +serialisation between implementations. ### Algorithm @@ -3889,12 +3943,12 @@ connection. Recovery is by reconnection; the Server presents a | Failure | When detected | | --- | --- | -| Agent set `RequiresPayloadTrustVerification` but the Server did not set `OffersPayloadTrustVerification`. | First `ServerToAgent` (capability bits visible at that time). | -| Server's first `ServerToAgent` does not include `trust_chain_response`. | First `ServerToAgent`. | -| `trust_chain_response.error_message` is non-empty. | First `ServerToAgent`. | -| Certificate chain fails X.509 path validation (expired certificate, unknown issuer, missing `id-kp-codeSigning` EKU, revoked certificate, etc.). | First `ServerToAgent`. | -| In-session `ServerToAgent` message lacks `signature`. | Any subsequent `ServerToAgent`. | -| In-session `signature` does not verify against the stored leaf certificate. | Any subsequent `ServerToAgent`. | +| Agent set `RequiresPayloadTrustVerification` but the Server did not send a `SignedServerToAgent` envelope (typically because the Server does not support the capability and sent a plain `ServerToAgent`). | First message received from the Server. | +| First `SignedServerToAgent` does not include `trust_chain_response`. | First message. | +| `trust_chain_response.error_message` is non-empty. | First message. | +| Certificate chain fails X.509 path validation (expired certificate, unknown issuer, missing `id-kp-codeSigning` EKU, revoked certificate, etc.). | First message. | +| In-session `SignedServerToAgent` lacks `signature`. | Any subsequent message. | +| In-session `signature` does not verify against the stored leaf certificate over the received `payload` bytes. | Any subsequent message. | | Stored leaf certificate's validity window has expired since the handshake. | The next verification after expiry. | ### Out of Scope