From 8da5ad48cf22bc1c929c385f872ca5d2c6f3ba1f Mon Sep 17 00:00:00 2001 From: Jaime Fullaondo Date: Mon, 11 May 2026 17:58:07 +0200 Subject: [PATCH 01/13] 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: 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 7a8dfcfa5c1436a65c79a8bea469014702ef75cc Mon Sep 17 00:00:00 2001 From: Jaime Fullaondo Date: Wed, 20 May 2026 00:31:43 +0200 Subject: [PATCH 02/13] 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. --- 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 From 04e8442afffbfda066d4afadad85c4e6e6f0ddd9 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Wed, 3 Jun 2026 17:44:40 -0400 Subject: [PATCH 03/13] Modify requirement for separate CAs --- specification.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/specification.md b/specification.md index 10bc260..7038a02 100644 --- a/specification.md +++ b/specification.md @@ -3601,8 +3601,7 @@ 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. +pre-configured trust anchor (a CA certificate). The mechanism allows OpAMP deployments to separate the _distribution server_ from the _authoritative source of OpAMP messages_. A compromised @@ -3653,10 +3652,14 @@ 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. +Agent's configuration). The payload trust anchor MUST NOT carry the `id-kp-serverAuth` +Extended Key Usage, ensuring it cannot double as a TLS CA certificate. +Operators MAY root both the TLS chain and the signing chain from the +same root CA, provided the signing intermediate and leaf certificates +carry only `id-kp-codeSigning` and the signing private key is stored +separately from the distribution server. The security boundary is the +combination of private-key isolation and EKU constraints — not a +requirement for a separate root CA. The Server is independently configured with a signing key and its corresponding certificate chain that validates back to the payload trust From 4ac3b0a0c0754ba7942206686539bb468b98bc31 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Wed, 3 Jun 2026 17:57:30 -0400 Subject: [PATCH 04/13] Add note on trust anchor rotation and retries --- specification.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/specification.md b/specification.md index 7038a02..924de7a 100644 --- a/specification.md +++ b/specification.md @@ -3672,6 +3672,14 @@ 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. +When the payload trust anchor approaches expiry or must be replaced, +rotation is handled by the same out-of-band mechanism used to provision +it initially (for example, updating the certificate file in the Agent's +configuration and restarting the Agent). Operators SHOULD plan for this +operational burden, such as using a long-lived root CA or automating +certificate distribution through their existing configuration-management +tooling. + ### Opt-in and Backwards Compatibility Message Attestation is a strict opt-in feature. @@ -3941,8 +3949,12 @@ 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. +connection. The Agent SHOULD reconnect using exponential backoff consistent +with normal OpAMP reconnection behaviour; on reconnection the Server presents +a (potentially rotated) chain on the new handshake. Because these failures +are detected locally by the Agent, no dedicated error-response field is +required — the Agent terminates the connection without waiting for a +`ServerToAgent.error_response`. | Failure | When detected | | --- | --- | From 187263b1268a7734b6c277cba4f54f742476343f Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Wed, 3 Jun 2026 18:11:46 -0400 Subject: [PATCH 05/13] Add supplementary-guidelines.md --- supplementary-guidelines.md | 86 +++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 supplementary-guidelines.md diff --git a/supplementary-guidelines.md b/supplementary-guidelines.md new file mode 100644 index 0000000..06b6e65 --- /dev/null +++ b/supplementary-guidelines.md @@ -0,0 +1,86 @@ +# Message Attestation: Supplementary Guidelines + +This document provides non-normative guidance for implementors and operators +deploying Message Attestation. It is a companion to the normative +[specification](specification.md). + +## Motivation + +OpAMP servers necessarily occupy a high-exposure position on the network: they +must be reachable by every member of the fleet. A compromise of the OpAMP server +(or a TLS-terminating proxy in front of it) would, without Message Attestation, +yield full control over all connected Agents. + +Message Attestation addresses this by separating the *distribution* of messages +from the *authorization* of messages. The distribution server remains at the +network edge; the signing authority lives deeper in the infrastructure, away +from the attack surface. + +## Example Deployment Architecture + +The following describes one example deployment. Actual deployments may be +simpler or more complex depending on organisational requirements. + +![Example deployment architecture](https://github.com/user-attachments/assets/20fb3567-ff84-47f3-9e42-367953881232) + +1. The Agent connects to the OpAMP server at the network edge. +2. The OpAMP server constructs a `ServerToAgent` payload to send to the Agent. +3. Before sending, the OpAMP server submits the payload to an internal policy + engine. +4. The policy engine evaluates whether the message is permitted for this Agent, + applying whatever organizational constraints are relevant. +5. If the message is approved, the policy engine submits a signature request to + a secure signer (typically a hardware security module). +6. The signer returns a signature over the payload bytes. +7. The OpAMP server wraps the payload and signature in a `SignedServerToAgent` + envelope and delivers it to the Agent. + +The key security property is structural isolation: the decision of *what is +allowed to be sent* is made by a component that is not directly reachable from +the network edge. An attacker who compromises the OpAMP server gains the ability +to send messages, but not the ability to have those messages accepted by Agents +with `RequiresPayloadTrustVerification` set — because the attacker cannot +produce valid signatures. + +## Policy Engine Constraints + +The policy engine can enforce any criteria the organization considers relevant. +Examples include: + +- Denying specific message types outright (e.g. no `ServerToAgentCommand`). +- Enforcing per-customer opt-ins and opt-outs. +- Enforcing security invariants (e.g. a specific pipeline must never be + disabled). +- Restricting which teams may push configuration changes. +- Requiring that only the latest approved version of a component may be + installed. + +These constraints could in principle be enforced client-side, but expressing +them flexibly would require significant complexity in Agent code. With Message +Attestation, the protocol defines a lightweight stamp ("this message was +authorized") and the Agent knows how to verify it; all bespoke policy logic +is maintained in the backend by the organization itself. + +## Operational Considerations + +### Trust Anchor Lifecycle + +The payload trust anchor is a root CA certificate provisioned out-of-band on +each Agent. It is deliberately not rotatable via OpAMP messages, to prevent a +compromised server from redirecting Agents to an attacker-controlled anchor. + +Operators should plan for trust anchor rotation using their existing +configuration-management tooling (e.g. Ansible, Chef, Puppet, or a secrets +manager). Using a long-lived root CA and shorter-lived signing intermediates +reduces rotation frequency for the trust anchor itself. + +### Certificate Revocation + +If a signing intermediate is compromised, standard X.509 revocation (CRL +distribution points or OCSP) handles it. The spec recommends enabling +revocation checking in the Agent's X.509 library. Operators may alternatively +use short-lived signing certificates as a substitute for active revocation. + +If the root payload trust anchor itself is compromised, it must be replaced via +the same out-of-band mechanism used to provision it. This is an intentional +design constraint: no OpAMP message may update the trust anchor. From d296b93d37bc5452ad5db30280b16896b838a7d7 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Thu, 4 Jun 2026 16:34:20 -0400 Subject: [PATCH 06/13] Apply suggestions from code review Co-authored-by: Pierre Prinetti <4400408+pierreprinetti@users.noreply.github.com> Co-authored-by: Tigran Najaryan <4194920+tigrannajaryan@users.noreply.github.com> --- specification.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/specification.md b/specification.md index 924de7a..49ba7f4 100644 --- a/specification.md +++ b/specification.md @@ -3665,7 +3665,7 @@ 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, +The payload trust anchor is NEVER sent from the Server to the Agent. 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 @@ -3764,12 +3764,15 @@ first such envelope carries the signing certificate chain in `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 - `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. + * Otherwise the Agent MUST perform X.509 certification path + validation as defined in [RFC 5280 §6](https://datatracker.ietf.org/doc/html/rfc5280#section-6), + using the pre-configured payload trust anchor as the sole trust + anchor, `certificate_chain` as the intermediate and leaf + certificates, and `id-kp-codeSigning` (`1.3.6.1.5.5.7.3.3`) in the + acceptable EKU set. If path validation fails for any reason — + including expired certificate, unknown issuer, missing + `id-kp-codeSigning` EKU, failed name/policy constraints, 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 From a10c4599f067b4432b81ea31984698fefc3e04ad Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Sat, 13 Jun 2026 14:57:16 -0400 Subject: [PATCH 07/13] Remove implied server-side validation requirement --- specification.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specification.md b/specification.md index 49ba7f4..bcac79a 100644 --- a/specification.md +++ b/specification.md @@ -3662,8 +3662,7 @@ combination of private-key isolation and EKU constraints — not a requirement for a separate root CA. The Server is independently configured with a signing key and its -corresponding certificate chain that validates back to the payload trust -anchor. +corresponding certificate chain rooted in the payload trust anchor. The payload trust anchor is NEVER sent from the Server to the Agent. In particular, no field of any `ServerToAgent` message — including From a5117405d29b290e65f489289c4f325e0d6a585f Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Sat, 13 Jun 2026 15:06:09 -0400 Subject: [PATCH 08/13] Require signature in every SignedServerToAgent message, including the first one --- proto/opamp.proto | 14 ++++++-------- specification.md | 36 +++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/proto/opamp.proto b/proto/opamp.proto index 7869f25..813487e 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -351,17 +351,15 @@ message SignedServerToAgent { // 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. + // Detached signature over the bytes of the payload field. MUST be + // present and verifiable on every SignedServerToAgent, including the + // first. There is no exception for the first message. 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 + // the signing certificate chain the Agent uses to validate the leaf + // certificate and verify signatures on all messages including this one. + // If the Agent set // RequiresPayloadTrustVerification but the first SignedServerToAgent // does not include a usable trust_chain_response, the Agent MUST // terminate the connection. diff --git a/specification.md b/specification.md index bcac79a..07dbb38 100644 --- a/specification.md +++ b/specification.md @@ -3743,10 +3743,9 @@ first such envelope carries the signing certificate chain in 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 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. + * MUST set `signature` to a valid detached signature over the + `payload` bytes on this first envelope and on every subsequent + envelope. There is no exception for the first message. * 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` @@ -3774,8 +3773,11 @@ first such envelope carries the signing certificate chain in 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 - `SignedServerToAgent`. + connection. + * The Agent MUST verify `signature` over the `payload` bytes using + the public key of the validated leaf certificate. If `signature` + is absent or verification fails, the Agent MUST terminate the + connection. * The Agent then unmarshals the `payload` bytes into a `ServerToAgent` and processes it normally. @@ -3804,17 +3806,18 @@ 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. +Detached signature over the bytes of `payload`. MUST be present and +verifiable on every `SignedServerToAgent`, including the first. There +is no exception for the first message — the Server signs the payload +immediately using the signing key whose certificate chain is carried in +`trust_chain_response`. ##### 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). +the signing certificate chain the Agent uses to validate the leaf +certificate and verify signatures on all messages including this one. +See [TrustChainResponse Message](#trustchainresponse-message). #### TrustChainResponse Message @@ -3887,8 +3890,7 @@ The Server produces a `SignedServerToAgent` as follows: 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: +The Agent verifies a received `SignedServerToAgent` as follows: 1. Parse the wire bytes as a `SignedServerToAgent`. Retain the `payload` field's raw bytes — these are the bytes the signature @@ -3964,8 +3966,8 @@ required — the Agent terminates the connection without waiting for a | 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. | +| `SignedServerToAgent` lacks `signature`. | Every message. | +| `signature` does not verify against the stored leaf certificate over the received `payload` bytes. | Every message. | | Stored leaf certificate's validity window has expired since the handshake. | The next verification after expiry. | ### Out of Scope From 481c420f466d4b176240be48675c26ecce191468 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Sun, 14 Jun 2026 17:29:55 -0400 Subject: [PATCH 09/13] Address PR comments --- proto/opamp.proto | 18 +++++++----- specification.md | 55 ++++++++++++++++++++--------------- supplementary-guidelines.md | 58 ++++++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 31 deletions(-) diff --git a/proto/opamp.proto b/proto/opamp.proto index 813487e..caffe1b 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -249,10 +249,14 @@ message ServerToAgent { // 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"; + // (SignedServerToAgent) to enable detached signing. Field numbers + // 14–16 are assigned to SignedServerToAgent.payload, .signature, + // and .trust_chain_response respectively, so that the two messages + // have non-overlapping field numbers and cannot be accidentally + // misinterpreted by a parser expecting the other type. All six + // field numbers are reserved here to prevent reuse on ServerToAgent. + reserved 12, 13, 14, 15, 16; + reserved "trust_chain_response", "signature", "payload"; } enum ServerToAgentFlags { @@ -349,12 +353,12 @@ 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; + bytes payload = 14; // Detached signature over the bytes of the payload field. MUST be // present and verifiable on every SignedServerToAgent, including the // first. There is no exception for the first message. - bytes signature = 2; + bytes signature = 15; // Sent only in the first SignedServerToAgent on a connection. Carries // the signing certificate chain the Agent uses to validate the leaf @@ -363,7 +367,7 @@ message SignedServerToAgent { // RequiresPayloadTrustVerification but the first SignedServerToAgent // does not include a usable trust_chain_response, the Agent MUST // terminate the connection. - TrustChainResponse trust_chain_response = 3; + TrustChainResponse trust_chain_response = 16; } // The OpAMPConnectionSettings message is a collection of fields which comprise an diff --git a/specification.md b/specification.md index 07dbb38..91f8a5a 100644 --- a/specification.md +++ b/specification.md @@ -3571,9 +3571,6 @@ Agent by default. The capabilities should be opt-in by the user. Any executable code that is part of a package should be signed 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) @@ -3593,6 +3590,10 @@ We 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. +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. + ## Message Attestation **Status: [Development]** @@ -3707,10 +3708,11 @@ 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. +A Server SHOULD only advertise `OffersPayloadTrustVerification` to +Agents that have set `RequiresPayloadTrustVerification`, as advertising +to non-opted-in Agents adds unnecessary bits to the capabilities field. +A Server MUST only wrap its outbound messages in `SignedServerToAgent` +on connections from Agents that have set `RequiresPayloadTrustVerification`. ### Capability Negotiation @@ -3718,7 +3720,7 @@ per-message cost for advertising the capability to non-opted-in Agents. | --- | --- | --- | | 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 | No | The Server does not send `SignedServerToAgent`. The Agent receives a plain `ServerToAgent` where it expected a `SignedServerToAgent` envelope. The Agent MUST treat this as a failure as described in [Failure Modes](#failure-modes). | | 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 @@ -3772,8 +3774,12 @@ first such envelope carries the signing certificate chain in `id-kp-codeSigning` EKU, failed name/policy constraints, 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. + certificate (or its public key) for the duration of the session. + For WebSocket transport the session ends when the connection closes. + For HTTP transport the session persists across polling requests and + ends when the Agent restarts or explicitly reconnects. On + reconnection, the handshake is repeated and the Server presents a + fresh certificate chain. * The Agent MUST verify `signature` over the `payload` bytes using the public key of the validated leaf certificate. If `signature` is absent or verification fails, the Agent MUST terminate the @@ -3786,13 +3792,13 @@ first such envelope carries the signing certificate chain in ```protobuf message SignedServerToAgent { // Serialised bytes of a ServerToAgent message. - bytes payload = 1; + bytes payload = 14; // Detached signature over the bytes of the payload field. - bytes signature = 2; + bytes signature = 15; // Sent only in the first SignedServerToAgent on a connection. - TrustChainResponse trust_chain_response = 3; + TrustChainResponse trust_chain_response = 16; } ``` @@ -3934,9 +3940,11 @@ they evolve. 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. + (`1.3.6.1.5.5.7.3.3`) in its list of allowed usages. TLS server + certificates carry `id-kp-serverAuth` instead; because the Agent + requires `id-kp-codeSigning` during chain validation, a TLS + certificate will fail the EKU check and cannot be repurposed as a + signing certificate. * Be within its validity window (`notBefore <= currentTime < notAfter`) at every verification. @@ -3952,13 +3960,14 @@ short-lived certificates as an alternative to active revocation. ### Failure Modes -Every failure listed below MUST cause the Agent to terminate the OpAMP -connection. The Agent SHOULD reconnect using exponential backoff consistent -with normal OpAMP reconnection behaviour; on reconnection the Server presents -a (potentially rotated) chain on the new handshake. Because these failures -are detected locally by the Agent, no dedicated error-response field is -required — the Agent terminates the connection without waiting for a -`ServerToAgent.error_response`. +Every failure listed below MUST cause the Agent to terminate the current +exchange without processing the received payload. For WebSocket transport, +the Agent closes the WebSocket connection. For HTTP transport, the Agent +discards the response body. In both cases, the Agent SHOULD reconnect using +exponential backoff consistent with normal OpAMP reconnection behaviour; on +reconnection the Server presents a (potentially rotated) chain on the new +handshake. Because these failures are detected locally by the Agent, no +dedicated error-response field is required. | Failure | When detected | | --- | --- | diff --git a/supplementary-guidelines.md b/supplementary-guidelines.md index 06b6e65..cd5f680 100644 --- a/supplementary-guidelines.md +++ b/supplementary-guidelines.md @@ -21,7 +21,23 @@ from the attack surface. The following describes one example deployment. Actual deployments may be simpler or more complex depending on organisational requirements. -![Example deployment architecture](https://github.com/user-attachments/assets/20fb3567-ff84-47f3-9e42-367953881232) +``` + Agent OpAMP Server Policy Engine Signer + (network edge) (internal) (HSM) + │ │ │ │ + │ (1) AgentToServer │ │ │ + ├──────────────────►│ │ │ + │ │ (3) payload │ │ + │ ├──────────────────►│ │ + │ │ │ (5) sign req │ + │ │ ├────────────────►│ + │ │ │ (6) signature │ + │ │ │◄────────────────┤ + │ │◄── signature ─────┤ │ + │ (7) Signed │ │ │ + │ ServerToAgent │ │ │ + │◄──────────────────┤ │ │ +``` 1. The Agent connects to the OpAMP server at the network edge. 2. The OpAMP server constructs a `ServerToAgent` payload to send to the Agent. @@ -61,6 +77,39 @@ Attestation, the protocol defines a lightweight stamp ("this message was authorized") and the Agent knows how to verify it; all bespoke policy logic is maintained in the backend by the organization itself. +## Certificate Hierarchy and Extended Key Usage + +X.509 certificates carry an *Extended Key Usage* (EKU) field that declares +what a certificate is permitted to do. Two EKU values are relevant to Message +Attestation: + +- **`id-kp-serverAuth`** — the certificate may authenticate a TLS server. TLS + clients check for this when establishing a secure connection. +- **`id-kp-codeSigning`** — the certificate may sign arbitrary data. Message + Attestation uses this EKU to mark signing certificates. + +The spec requires the signing leaf certificate to carry `id-kp-codeSigning` and +prohibits `id-kp-serverAuth` on it. This means a TLS certificate cannot be +repurposed to sign OpAMP messages, and a signing certificate cannot be used as +a TLS server certificate. + +### Do I need a separate root CA for signing? + +No. The same root CA may issue both TLS and signing certificates, provided: + +1. Each intermediate and leaf certificate carries only the appropriate EKU. +2. The signing private key is stored separately from the distribution server — + for example, in an HSM or secrets manager the distribution server process + cannot reach. + +The security boundary is *key isolation*, not *CA isolation*. An attacker who +compromises the distribution server but cannot access the signing private key +cannot forge valid signed messages, regardless of whether TLS and signing +certificates share a root. + +Operators who prefer strict CA separation may use separate roots, but it is not +required. + ## Operational Considerations ### Trust Anchor Lifecycle @@ -74,6 +123,13 @@ configuration-management tooling (e.g. Ansible, Chef, Puppet, or a secrets manager). Using a long-lived root CA and shorter-lived signing intermediates reduces rotation frequency for the trust anchor itself. +An alternative approach is to compile the trust anchor directly into the Agent +binary. In this model, trust anchor rotation is handled by distributing a new +Agent version — the same mechanism used for any other Agent update. This can be +simpler to operate in environments where Agent binary updates are already +automated, and it avoids the need for a separate certificate-management +pipeline. + ### Certificate Revocation If a signing intermediate is compromised, standard X.509 revocation (CRL From 4cfb6b4aaa73a4323a3150bf730e8499fe53cf90 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Sun, 14 Jun 2026 17:44:50 -0400 Subject: [PATCH 10/13] Remove "empty ServerToAgent" --- specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification.md b/specification.md index 91f8a5a..123f678 100644 --- a/specification.md +++ b/specification.md @@ -3750,7 +3750,7 @@ first such envelope carries the signing certificate chain in envelope. There is no exception for the first message. * 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` + signing not been negotiated (for example, a `ServerToAgent` acknowledging the Agent's status report, or a `ServerToAgent` carrying initial `remote_config`). From 81eb6fe6777573e3425ff6d29676887803902a07 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Mon, 15 Jun 2026 15:25:15 -0400 Subject: [PATCH 11/13] Update certificate format to use PEM --- proto/opamp.proto | 16 ++++++---------- specification.md | 34 ++++++++++++++++------------------ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/proto/opamp.proto b/proto/opamp.proto index caffe1b..c79f0f2 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -318,16 +318,12 @@ enum ServerCapabilities { // 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; + // PEM-encoded 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. Multiple certificates are concatenated in a single + // PEM blob, consistent with the encoding used by TLSCertificate. + bytes 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 diff --git a/specification.md b/specification.md index 123f678..4d008cd 100644 --- a/specification.md +++ b/specification.md @@ -3737,10 +3737,10 @@ first such envelope carries the signing certificate chain in 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. + `trust_chain_response.certificate_chain` as a PEM blob 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, @@ -3764,10 +3764,11 @@ first such envelope carries the signing certificate chain in `SignedServerToAgent`.) * If `trust_chain_response.error_message` is non-empty, the Agent MUST terminate the connection. - * Otherwise the Agent MUST perform X.509 certification path + * Otherwise the Agent MUST decode the `certificate_chain` PEM blob + into individual certificates and perform X.509 certification path validation as defined in [RFC 5280 §6](https://datatracker.ietf.org/doc/html/rfc5280#section-6), using the pre-configured payload trust anchor as the sole trust - anchor, `certificate_chain` as the intermediate and leaf + anchor, the decoded certificates as the intermediate and leaf certificates, and `id-kp-codeSigning` (`1.3.6.1.5.5.7.3.3`) in the acceptable EKU set. If path validation fails for any reason — including expired certificate, unknown issuer, missing @@ -3829,16 +3830,13 @@ See [TrustChainResponse 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 + // PEM-encoded 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; + // its pre-configured payload trust anchor. Multiple certificates are + // concatenated in a single PEM blob, consistent with the encoding + // used by TLSCertificate. + bytes certificate_chain = 1; // Human-readable error message indicating why the Server could not // satisfy the trust chain request. If error_message is non-empty, @@ -3849,10 +3847,10 @@ message TrustChainResponse { ##### 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. +PEM-encoded certificate chain, ordered from the first intermediate down +to the signing leaf certificate. The root certificate is excluded. +Multiple certificates are concatenated as a single PEM blob, consistent +with the encoding used by `TLSCertificate.cert`. ##### TrustChainResponse.error_message From fbcf5274b286ae70ed4f0e94c41e46d14a7a2ebe Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Tue, 30 Jun 2026 22:52:57 -0400 Subject: [PATCH 12/13] Add optional TOFU flow, SAN entry requirement --- proto/opamp.proto | 22 ++++++++++ specification.md | 107 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/proto/opamp.proto b/proto/opamp.proto index c79f0f2..1be87e0 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -329,6 +329,19 @@ message TrustChainResponse { // satisfy the trust chain request. If error_message is set, the Agent // MUST terminate the connection. string error_message = 2; + + // PEM-encoded root CA certificate used as the payload trust anchor. + // Set only during Trust On First Use (TOFU) enrollment: the Server + // includes this when the Agent has advertised + // AgentCapabilities_AcceptsPayloadTrustAnchorTOFU and the Agent has no + // pre-configured trust anchor. The Agent MUST persist this certificate + // and use it as the payload trust anchor for all subsequent connections. + // The Agent MUST NOT update a previously persisted or operator-configured + // trust anchor — if the Agent already has a trust anchor this field MUST + // be ignored. To rotate a TOFU-enrolled anchor the operator deletes the + // persisted anchor file and restarts the Agent (out-of-band rotation). + // See the Trust On First Use (TOFU) section of the specification. + bytes tofu_trust_anchor = 3; } // SignedServerToAgent wraps a ServerToAgent message when the payload trust @@ -860,6 +873,15 @@ enum AgentCapabilities { // the connection. See the Message Attestation section of the specification. // Status: [Development] AgentCapabilities_RequiresPayloadTrustVerification = 0x00010000; + // The Agent supports Trust On First Use (TOFU) enrollment for the payload + // trust anchor. When set alongside + // AgentCapabilities_RequiresPayloadTrustVerification it signals that the + // Agent has no pre-configured trust anchor and asks the Server to include + // the root CA in trust_chain_response.tofu_trust_anchor so the Agent can + // bootstrap and persist it. MUST NOT be set if the Agent already has a + // persisted or operator-configured trust anchor. + // Status: [Development] + AgentCapabilities_AcceptsPayloadTrustAnchorTOFU = 0x00020000; // Add new capabilities here, continuing with the least significant unused bit. } diff --git a/specification.md b/specification.md index 4d008cd..a20c42e 100644 --- a/specification.md +++ b/specification.md @@ -3652,8 +3652,8 @@ Message Attestation does **not** address: 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 carry the `id-kp-serverAuth` +implementation-specific configuration (for example, a file path or an +inline PEM value in the Agent's configuration). The payload trust anchor MUST NOT carry the `id-kp-serverAuth` Extended Key Usage, ensuring it cannot double as a TLS CA certificate. Operators MAY root both the TLS chain and the signing chain from the same root CA, provided the signing intermediate and leaf certificates @@ -3665,12 +3665,15 @@ requirement for a separate root CA. The Server is independently configured with a signing key and its corresponding certificate chain rooted in the payload trust anchor. -The payload trust anchor is NEVER sent from the Server to the Agent. In particular, -no field of any `ServerToAgent` message — including +The payload trust anchor is NEVER sent from the Server to the Agent, with one +exception: Agents that have not yet enrolled a trust anchor MAY opt in to Trust +On First Use (TOFU) enrollment, described in +[Trust On First Use (TOFU)](#trust-on-first-use-tofu). +In all other cases — including when a TOFU anchor has already been persisted — 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. +update or replace the Agent's payload trust anchor. This constraint prevents a +compromised Server from rotating the Agent onto an attacker-controlled trust anchor. When the payload trust anchor approaches expiry or must be replaced, rotation is handled by the same out-of-band mechanism used to provision @@ -3678,7 +3681,82 @@ it initially (for example, updating the certificate file in the Agent's configuration and restarting the Agent). Operators SHOULD plan for this operational burden, such as using a long-lived root CA or automating certificate distribution through their existing configuration-management -tooling. +tooling. The same out-of-band rotation applies to TOFU-enrolled anchors: +to rotate, delete the persisted anchor file and restart the Agent. + +### Trust On First Use (TOFU) + +Agents that do not have a pre-configured payload trust anchor may opt in to +Trust On First Use (TOFU) enrollment. On the first connection the Server +delivers the root CA certificate in `TrustChainResponse.tofu_trust_anchor`; +the Agent persists it as its payload trust anchor and uses it for all +subsequent connections. TOFU is **disabled by default** and requires explicit +operator configuration. + +#### Security implications + +TOFU provides **no security on the first connection**: if the first connection +is intercepted or the Server is compromised at enrollment time, the Agent will +pin to an attacker-controlled anchor. Operators SHOULD prefer pre-configuring the +trust anchor through existing configuration-management tooling (Ansible, Chef, +Puppet, a secrets manager, or a compiled-in certificate). Use TOFU only when +out-of-band provisioning is impractical. + +TOFU is also unsuitable for **stateless or short-lived Agents** (for example, +Agents running in ephemeral containers) that cannot persist state across restarts. +Such Agents would re-enroll on every startup, effectively providing no +protection. + +#### TOFU capability advertisement + +An Agent opts in to TOFU enrollment by setting both +`AgentCapabilities_RequiresPayloadTrustVerification` and +`AgentCapabilities_AcceptsPayloadTrustAnchorTOFU` in its `AgentToServer.capabilities`. +An Agent MUST NOT set `AcceptsPayloadTrustAnchorTOFU` if it already has a +persisted or pre-configured trust anchor — it MUST advertise only +`RequiresPayloadTrustVerification` in that case. + +#### TOFU enrollment flow + +1. The Agent, having no trust anchor, sets both capability bits. +2. The Server recognises `AcceptsPayloadTrustAnchorTOFU` and includes the root CA + PEM in `TrustChainResponse.tofu_trust_anchor` of the first `SignedServerToAgent`. +3. The Agent receives the envelope, validates the certificate chain against the + delivered root CA, verifies the signature, and — if all checks pass — persists + `tofu_trust_anchor` as its payload trust anchor. +4. On all subsequent connections the Agent advertises only + `RequiresPayloadTrustVerification` (not `AcceptsPayloadTrustAnchorTOFU`) and + uses the persisted anchor as if it were a pre-configured trust anchor. + +The Server MUST include `tofu_trust_anchor` when the Agent has set +`AcceptsPayloadTrustAnchorTOFU` and the Server holds the root CA. If the Server +cannot provide the root CA it MUST set `error_message` and omit +`tofu_trust_anchor`, causing the Agent to terminate the connection. + +#### TOFU anchor immutability + +The Agent MUST NOT update a previously persisted or operator-configured trust +anchor. If `tofu_trust_anchor` is delivered on a connection where the Agent +already has a trust anchor, the Agent MUST ignore it. This constraint applies +equally to TOFU-enrolled anchors and statically-configured anchors. + +**Rationale:** allowing a server-side anchor update would recreate the same attack +vector that Message Attestation is designed to prevent. If the Server is +compromised after TOFU enrollment, it must not be able to re-enroll the Agent +onto an attacker-controlled anchor. + +#### TOFU anchor rotation + +To replace a TOFU-enrolled trust anchor (for example, when the enrolled CA is +approaching expiry or has been compromised), the operator deletes the persisted +anchor file and restarts the Agent. On restart the Agent has no anchor and will +re-enroll using the new CA configured on the Server. This is deliberately +out-of-band — the same mechanism used to rotate a statically-configured anchor. + +Operators SHOULD monitor the expiry of the enrolled CA and schedule rotation +before the certificate expires. An expired enrolled CA will cause attestation +failures on the next connection, rendering the Agent unable to receive any +`ServerToAgent` messages until the anchor is rotated. ### Opt-in and Backwards Compatibility @@ -3774,6 +3852,11 @@ first such envelope carries the signing certificate chain in including expired certificate, unknown issuer, missing `id-kp-codeSigning` EKU, failed name/policy constraints, or revocation — the Agent MUST terminate the connection. + * The Agent MUST verify that the leaf certificate's Subject + Alternative Name (SAN) extension contains a `dNSName` or + `iPAddress` entry matching the OpAMP server the Agent is connected + to (using the same hostname matching rules as TLS). If the SAN + check fails 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 session. For WebSocket transport the session ends when the connection closes. @@ -3945,6 +4028,14 @@ The signing leaf certificate MUST: signing certificate. * Be within its validity window (`notBefore <= currentTime < notAfter`) at every verification. +* Contain a Subject Alternative Name (SAN) extension with a `dNSName` + entry that matches the hostname of the OpAMP distribution server the + Agent is connected to, or an `iPAddress` entry that matches the + server's IP address when the Agent connects by IP. During the + connection-time handshake the Agent MUST verify this match in + addition to standard X.509 path validation. This binds the signing + certificate to a specific deployment and prevents a key valid for one + server from being accepted by Agents of a different server. The certificate chain (intermediates plus leaf) MUST chain to the pre-configured payload trust anchor. The trust anchor itself is From cd65d600994d28e3f084f9751bc7793f3b510d63 Mon Sep 17 00:00:00 2001 From: Stanley Liu Date: Tue, 30 Jun 2026 22:54:13 -0400 Subject: [PATCH 13/13] Update specification.md Co-authored-by: Pierre Prinetti <4400408+pierreprinetti@users.noreply.github.com> --- specification.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specification.md b/specification.md index a20c42e..d6d4f8f 100644 --- a/specification.md +++ b/specification.md @@ -3998,9 +3998,7 @@ serialisation between implementations. ### Algorithm -The signing algorithm is determined by the leaf certificate's -`signatureAlgorithm` field. The OpAMP protocol does not negotiate -algorithms. +The signing algorithm is determined by the leaf certificate's public key (subjectPublicKeyInfo), paired with the hash function specified for that key type in the list below. The OpAMP protocol does not negotiate algorithms. Implementations SHOULD support, at minimum, the following algorithms: