feat(site-runtime): CertStoreActor exports trusted certs (thumbprint+DER) for node reconciliation (UA1 groundwork)
This commit is contained in:
@@ -82,6 +82,30 @@ public record RemoveCertFromLocalStore(string Thumbprint);
|
|||||||
/// <summary>Per-node: enumerate the local trusted-peer and rejected stores.</summary>
|
/// <summary>Per-node: enumerate the local trusted-peer and rejected stores.</summary>
|
||||||
public record ListLocalCerts();
|
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>
|
/// <summary>
|
||||||
/// Per-node ack for a <see cref="WriteCertToLocalStore"/>, <see cref="RemoveCertFromLocalStore"/>
|
/// Per-node ack for a <see cref="WriteCertToLocalStore"/>, <see cref="RemoveCertFromLocalStore"/>
|
||||||
/// or <see cref="ListLocalCerts"/> operation.
|
/// or <see cref="ListLocalCerts"/> operation.
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class CertStoreActor : ReceiveActor
|
|||||||
Receive<WriteCertToLocalStore>(HandleWrite);
|
Receive<WriteCertToLocalStore>(HandleWrite);
|
||||||
Receive<RemoveCertFromLocalStore>(HandleRemove);
|
Receive<RemoveCertFromLocalStore>(HandleRemove);
|
||||||
Receive<ListLocalCerts>(_ => HandleList());
|
Receive<ListLocalCerts>(_ => HandleList());
|
||||||
|
Receive<ExportLocalTrustedCerts>(_ => HandleExport());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,6 +134,47 @@ public class CertStoreActor : ReceiveActor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleExport()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var certs = new List<ExportedCert>();
|
||||||
|
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<ExportedCert>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<TrustedCertInfo> EnumerateStore(string storeDir, bool rejected)
|
private IEnumerable<TrustedCertInfo> EnumerateStore(string storeDir, bool rejected)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(storeDir))
|
if (!Directory.Exists(storeDir))
|
||||||
|
|||||||
@@ -106,6 +106,27 @@ public class CertStoreActorTests : TestKit, IDisposable
|
|||||||
emptyAck.Certs!.Should().NotContain(c => c.Thumbprint == thumbprint);
|
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<LocalCertOpAck>().Success.Should().BeTrue();
|
||||||
|
|
||||||
|
// Export — thumbprint + raw DER bytes come back for reconciliation.
|
||||||
|
actor.Tell(new ExportLocalTrustedCerts());
|
||||||
|
var export = ExpectMsg<LocalCertExport>();
|
||||||
|
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]
|
[Fact]
|
||||||
public void Write_InvalidBase64_ReturnsFailureAck()
|
public void Write_InvalidBase64_ReturnsFailureAck()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user