diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/CertTrustCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/CertTrustCommands.cs
index 7f8dcf31..30b5e6dc 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/CertTrustCommands.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/CertTrustCommands.cs
@@ -82,6 +82,30 @@ public record RemoveCertFromLocalStore(string Thumbprint);
/// Per-node: enumerate the local trusted-peer and rejected stores.
public record ListLocalCerts();
+///
+/// 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.
+///
+public record ExportLocalTrustedCerts();
+
+///
+/// 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.
+///
+/// The certificate thumbprint.
+/// The certificate's raw DER encoding.
+public record ExportedCert(string Thumbprint, byte[] DerBytes);
+
+///
+/// Per-node reply to : the local trusted-peer
+/// store's certificates, or a failure with the IO error.
+///
+/// True if the trusted store was enumerated successfully.
+/// The error message on failure, otherwise null.
+/// Exported trusted certificates, empty on failure.
+public record LocalCertExport(bool Success, string? Error, IReadOnlyList Certs);
+
///
/// Per-node ack for a ,
/// or operation.
diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/CertStoreActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/CertStoreActor.cs
index 96d9a3af..17f5739d 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/CertStoreActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/CertStoreActor.cs
@@ -51,6 +51,7 @@ public class CertStoreActor : ReceiveActor
Receive(HandleWrite);
Receive(HandleRemove);
Receive(_ => HandleList());
+ Receive(_ => HandleExport());
}
///
@@ -133,6 +134,47 @@ public class CertStoreActor : ReceiveActor
}
}
+ ///
+ /// Exports the trusted-peer store (thumbprint + raw DER) so the Deployment
+ /// Manager singleton can reconcile the trusted set onto a (re)joining node.
+ /// Trusted store only — rejected certs are never pushed to peers.
+ ///
+ private void HandleExport()
+ {
+ try
+ {
+ var certs = new List();
+ if (Directory.Exists(_trustedStoreDir))
+ {
+ foreach (var file in Directory.EnumerateFiles(_trustedStoreDir)
+ .Where(f => f.EndsWith(".der", StringComparison.OrdinalIgnoreCase)
+ || f.EndsWith(".crt", StringComparison.OrdinalIgnoreCase)))
+ {
+ try
+ {
+ var der = File.ReadAllBytes(file);
+ // Load only to derive the canonical thumbprint (the store
+ // filename); the DER bytes shipped are the raw file bytes.
+ using var cert = X509CertificateLoader.LoadCertificate(der);
+ certs.Add(new ExportedCert(cert.Thumbprint, der));
+ }
+ catch (Exception ex)
+ {
+ // A malformed file must not abort the whole export.
+ _log.Warning(ex, "Skipping unreadable certificate file {File} during export", file);
+ }
+ }
+ }
+
+ Sender.Tell(new LocalCertExport(true, null, certs));
+ }
+ catch (Exception ex)
+ {
+ _log.Warning(ex, "Failed to export trusted certificates from PKI store");
+ Sender.Tell(new LocalCertExport(false, ex.Message, Array.Empty()));
+ }
+ }
+
private IEnumerable EnumerateStore(string storeDir, bool rejected)
{
if (!Directory.Exists(storeDir))
diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/CertStoreActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/CertStoreActorTests.cs
index 7b429346..15ab81f9 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/CertStoreActorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/CertStoreActorTests.cs
@@ -106,6 +106,27 @@ public class CertStoreActorTests : TestKit, IDisposable
emptyAck.Certs!.Should().NotContain(c => c.Thumbprint == thumbprint);
}
+ [Fact]
+ public void ExportLocalTrustedCerts_ReturnsThumbprintAndDerBytes()
+ {
+ var (derBase64, thumbprint) = BuildSelfSignedCert();
+ var expectedDer = Convert.FromBase64String(derBase64);
+ var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));
+
+ // Seed one trusted cert via the normal write path.
+ actor.Tell(new WriteCertToLocalStore(derBase64, thumbprint));
+ ExpectMsg().Success.Should().BeTrue();
+
+ // Export — thumbprint + raw DER bytes come back for reconciliation.
+ actor.Tell(new ExportLocalTrustedCerts());
+ var export = ExpectMsg();
+ export.Success.Should().BeTrue();
+ export.Error.Should().BeNull();
+ var entry = export.Certs.Should().ContainSingle().Subject;
+ entry.Thumbprint.Should().Be(thumbprint);
+ entry.DerBytes.Should().Equal(expectedDer);
+ }
+
[Fact]
public void Write_InvalidBase64_ReturnsFailureAck()
{