Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/CertTrustCommands.cs
T
Joseph Doherty f5eed5e8fd feat(audit): persist OPC UA certificate-trust decisions to the central audit log (ScadaBridge#43)
New AuditChannel.Security with AuditKind.CertTrust / CertTrustRemove (appended, ordinals stable).
Both central entry points - the management actor handlers and the Central UI CertManagementService -
write a best-effort ICentralAuditWriter row after the site outcome is known (Delivered when all
reachable site nodes acked, Failed otherwise, incl. relay timeouts), naming actor, site, thumbprint
and connection. Shared builder: CertTrustAuditEvents.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:17:03 -05:00

137 lines
7.7 KiB
C#

namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
// ─────────────────────────────────────────────────────────────────────────────
// OPC UA server-certificate trust management.
//
// Cert trust is SITE-LOCAL: there is no central persistence of trusted server
// certificates. The trusted-peer PKI store lives on each site node's file
// system, so a trust/remove decision must reach BOTH site nodes (node-a and
// node-b) or the two PKI stores diverge across failover.
//
// The public commands below are handled by the site Deployment Manager
// singleton (active node only). For trust/remove it broadcasts the
// corresponding per-node internal message to the per-node CertStoreActor on
// EVERY site cluster node; for list it answers from the local node.
//
// The decision itself IS recorded centrally (Gitea #43): both central entry points (the
// management actor and the Central UI CertManagementService) write an AuditChannel.Security
// AuditLog row (AuditKind.CertTrust / CertTrustRemove) naming the actor, site and thumbprint,
// with Delivered/Failed per the aggregate site outcome. Only the certificate itself stays site-local.
// ─────────────────────────────────────────────────────────────────────────────
/// <summary>
/// Trust an OPC UA server certificate at every site node. Carries the raw
/// DER bytes (base64-encoded) so the certificate can be written into each
/// node's trusted-peer PKI store. The thumbprint is the store filename key.
/// </summary>
/// <param name="ConnectionName">The data-connection the certificate was captured from (diagnostics / correlation only).</param>
/// <param name="DerBase64">The server certificate's DER encoding, base64-encoded.</param>
/// <param name="Thumbprint">The certificate thumbprint — used as the store filename key.</param>
/// <param name="SiteIdentifier">
/// Target site identifier — REQUIRED when the command is invoked via the management
/// HTTP API/CLI (the actor must route it to a site); ignored on the site side, where
/// routing already happened. Additive per the message-contract evolution rules.
/// </param>
public record TrustServerCertCommand(string ConnectionName, string DerBase64, string Thumbprint, string? SiteIdentifier = null);
/// <summary>
/// Remove a previously-trusted OPC UA server certificate from every site
/// node's trusted-peer PKI store, identified by thumbprint.
/// </summary>
/// <param name="Thumbprint">The thumbprint of the certificate to remove.</param>
/// <param name="SiteIdentifier">
/// Target site identifier — REQUIRED when the command is invoked via the management
/// HTTP API/CLI (the actor must route it to a site); ignored on the site side, where
/// routing already happened. Additive per the message-contract evolution rules.
/// </param>
public record RemoveServerCertCommand(string Thumbprint, string? SiteIdentifier = null);
/// <summary>
/// List the certificates currently present in this site's trusted-peer and
/// rejected PKI stores. Answered from the local node (the Deployment Manager
/// singleton's own node).
/// </summary>
/// <param name="SiteIdentifier">
/// Target site identifier — REQUIRED when the command is invoked via the management
/// HTTP API/CLI (the actor must route it to a site); ignored on the site side, where
/// routing already happened. Additive per the message-contract evolution rules.
/// </param>
public record ListServerCertsCommand(string? SiteIdentifier = null);
/// <summary>
/// Read-only projection of a certificate found in a site PKI store.
/// </summary>
/// <param name="Thumbprint">The certificate thumbprint.</param>
/// <param name="Subject">The certificate subject distinguished name.</param>
/// <param name="Issuer">The certificate issuer distinguished name.</param>
/// <param name="NotBeforeUtc">Validity start (UTC).</param>
/// <param name="NotAfterUtc">Validity end (UTC).</param>
/// <param name="Rejected">True if the certificate is in the rejected store; false if trusted.</param>
public record TrustedCertInfo(
string Thumbprint,
string Subject,
string Issuer,
DateTime NotBeforeUtc,
DateTime NotAfterUtc,
bool Rejected);
/// <summary>
/// Aggregate result of a cert-trust command. For trust/remove, <paramref name="Success"/>
/// reflects whether every reachable site node acked; <paramref name="Error"/> carries the
/// first node error (or a partial-failure note when a node did not ack in time).
/// For list, <paramref name="Certs"/> carries the local node's store contents.
/// </summary>
/// <param name="Success">True when the operation succeeded on all targeted nodes.</param>
/// <param name="Error">First error encountered, or null on success.</param>
/// <param name="Certs">Listed certificates (list command only), otherwise null.</param>
public record CertTrustResult(bool Success, string? Error, IReadOnlyList<TrustedCertInfo>? Certs);
// ── Per-node internal messages (CertStoreActor wire protocol) ──
// Sent by the Deployment Manager singleton to each site node's CertStoreActor,
// or used directly in tests. Not part of the public management surface.
/// <summary>Per-node: decode <paramref name="DerBase64"/> and write it into the local trusted-peer store as <c>&lt;Thumbprint&gt;.der</c>.</summary>
/// <param name="DerBase64">The certificate's DER encoding, base64-encoded.</param>
/// <param name="Thumbprint">The thumbprint used as the store filename key.</param>
public record WriteCertToLocalStore(string DerBase64, string Thumbprint);
/// <summary>Per-node: delete the trusted-peer store file matching <paramref name="Thumbprint"/>.</summary>
/// <param name="Thumbprint">The thumbprint of the certificate to remove.</param>
public record RemoveCertFromLocalStore(string Thumbprint);
/// <summary>Per-node: enumerate the local trusted-peer and rejected stores.</summary>
public record ListLocalCerts();
/// <summary>
/// Per-node: export the trusted-peer store contents (thumbprint + raw DER bytes)
/// so the Deployment Manager singleton can reconcile the trusted set onto a
/// (re)joining site node. Trusted store only — rejected certs are never pushed.
/// </summary>
public record ExportLocalTrustedCerts();
/// <summary>
/// One exported trusted certificate: its thumbprint (store filename key) and the
/// raw DER bytes needed to re-write it into another node's trusted-peer store.
/// </summary>
/// <param name="Thumbprint">The certificate thumbprint.</param>
/// <param name="DerBytes">The certificate's raw DER encoding.</param>
public record ExportedCert(string Thumbprint, byte[] DerBytes);
/// <summary>
/// Per-node reply to <see cref="ExportLocalTrustedCerts"/>: the local trusted-peer
/// store's certificates, or a failure with the IO error.
/// </summary>
/// <param name="Success">True if the trusted store was enumerated successfully.</param>
/// <param name="Error">The error message on failure, otherwise null.</param>
/// <param name="Certs">Exported trusted certificates, empty on failure.</param>
public record LocalCertExport(bool Success, string? Error, IReadOnlyList<ExportedCert> Certs);
/// <summary>
/// Per-node ack for a <see cref="WriteCertToLocalStore"/>, <see cref="RemoveCertFromLocalStore"/>
/// or <see cref="ListLocalCerts"/> operation.
/// </summary>
/// <param name="Success">True if the local store operation succeeded.</param>
/// <param name="Error">The error message on failure, otherwise null.</param>
/// <param name="Certs">Listed certificates (list only), otherwise null.</param>
public record LocalCertOpAck(bool Success, string? Error, IReadOnlyList<TrustedCertInfo>? Certs);