diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e6a73..e67a02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # 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 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 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.18.0 * Fix OpAMP port in example by @tigrannajaryan in https://github.com/open-telemetry/opamp-spec/pull/331 diff --git a/proto/opamp.proto b/proto/opamp.proto index 4772678..1be87e0 100644 --- a/proto/opamp.proto +++ b/proto/opamp.proto @@ -244,6 +244,19 @@ message ServerToAgent { // A custom message sent from the Server to an Agent. // Status: [Development] CustomMessage custom_message = 11; + + // 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. 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 { @@ -290,10 +303,82 @@ 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 { + // 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 + // 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 +// 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 = 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 = 15; + + // Sent only in the first SignedServerToAgent on a connection. Carries + // 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. + TrustChainResponse trust_chain_response = 16; +} + // 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. @@ -782,6 +867,21 @@ 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; + // 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 092f26c..d6d4f8f 100644 --- a/specification.md +++ b/specification.md @@ -225,6 +225,24 @@ 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) + + [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) + * [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 +674,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 +941,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. } @@ -3541,8 +3570,8 @@ 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. +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 +3590,507 @@ 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]** + +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). + +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 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 +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 rooted in the payload trust anchor. + +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 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 +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. 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 + +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 `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 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 + +| Agent `Requires` | Server `Offers` | Behaviour | +| --- | --- | --- | +| 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. 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 + +When the Agent has set `RequiresPayloadTrustVerification` and the Server +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, on receiving the Agent's first message and recognising + the capability: + * Sends its first `SignedServerToAgent` containing + `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, + `certificate_chain` SHOULD be empty. + * 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, a `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 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, 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 + `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. + 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 + connection. + * 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 = 14; + + // Detached signature over the bytes of the payload field. + bytes signature = 15; + + // Sent only in the first SignedServerToAgent on a connection. + TrustChainResponse trust_chain_response = 16; +} +``` + +##### 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`. 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 uses to validate the leaf +certificate and verify signatures on all messages including this one. +See [TrustChainResponse Message](#trustchainresponse-message). + +#### TrustChainResponse Message + +```protobuf +message TrustChainResponse { + // 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 non-empty, + // the Agent MUST terminate the connection. + string error_message = 2; +} +``` + +##### TrustChainResponse.certificate_chain + +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 + +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 + +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. 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` 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. +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 + +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: + +* 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. 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. +* 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 +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 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 | +| --- | --- | +| 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. | +| `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 + +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 diff --git a/supplementary-guidelines.md b/supplementary-guidelines.md new file mode 100644 index 0000000..cd5f680 --- /dev/null +++ b/supplementary-guidelines.md @@ -0,0 +1,142 @@ +# 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. + +``` + 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. +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. + +## 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 + +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. + +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 +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.