fix(client): use default certificate lifetime without rotating persisted keys
This commit is contained in:
+3
-1
@@ -71,7 +71,9 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
ApplicationType = ApplicationType.Client,
|
||||
ApplicationConfiguration = config
|
||||
};
|
||||
await app.CheckApplicationInstanceCertificatesAsync(false, lifeTimeInMonths: 2048, ct: ct);
|
||||
// Key size is configured separately; this argument is a lifetime in months.
|
||||
// Keep the SDK creation default and reuse any valid certificate already in the store.
|
||||
await app.CheckApplicationInstanceCertificatesAsync(false, ct: ct);
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
+58
@@ -1,4 +1,7 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Configuration;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
|
||||
@@ -9,6 +12,61 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters;
|
||||
/// <summary>Uses the real configuration factory so fake sessions cannot conceal SDK initialization failures.</summary>
|
||||
public sealed class DefaultApplicationConfigurationFactoryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task New_certificates_use_the_default_lifetime_and_existing_private_keys_are_reused(bool seedLongerCertificate)
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var root = Path.Combine(Path.GetTempPath(), $"otopcua-certificate-lifetime-{Guid.NewGuid():N}");
|
||||
var settings = new ConnectionSettings
|
||||
{
|
||||
EndpointUrl = "opc.tcp://localhost:4840", CertificateStorePath = root,
|
||||
SecurityMode = SecurityMode.None, AutoAcceptCertificates = false,
|
||||
};
|
||||
var factory = new DefaultApplicationConfigurationFactory();
|
||||
ApplicationConfiguration? current = null;
|
||||
try
|
||||
{
|
||||
if (seedLongerCertificate)
|
||||
{
|
||||
var seed = await factory.CreateAsync(settings, ct);
|
||||
await using var application = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) { ApplicationConfiguration = seed };
|
||||
(await application.CheckApplicationInstanceCertificatesAsync(true, lifeTimeInMonths: 60, ct)).ShouldBeTrue();
|
||||
}
|
||||
settings.SecurityMode = SecurityMode.SignAndEncrypt;
|
||||
current = await factory.CreateAsync(settings, ct);
|
||||
var certPath = Directory.GetFiles(Path.Combine(root, "own", "certs"), "*.der").ShouldHaveSingleItem();
|
||||
var privatePath = Directory.GetFiles(Path.Combine(root, "own", "private"), "*.pfx").ShouldHaveSingleItem();
|
||||
using var certificate = X509CertificateLoader.LoadCertificateFromFile(certPath);
|
||||
using var privateCertificate = X509CertificateLoader.LoadPkcs12FromFile(privatePath, null, OperatingSystem.IsMacOS() ? X509KeyStorageFlags.DefaultKeySet : X509KeyStorageFlags.EphemeralKeySet);
|
||||
privateCertificate.HasPrivateKey.ShouldBeTrue();
|
||||
certificate.Thumbprint.ShouldBe(privateCertificate.Thumbprint);
|
||||
using var rsa = certificate.GetRSAPublicKey();
|
||||
rsa.ShouldNotBeNull().KeySize.ShouldBeGreaterThanOrEqualTo(2048);
|
||||
if (seedLongerCertificate)
|
||||
certificate.NotAfter.ToUniversalTime().ShouldBeGreaterThan(DateTime.UtcNow.AddYears(4));
|
||||
else
|
||||
{
|
||||
certificate.NotAfter.ToUniversalTime().ShouldBeGreaterThan(DateTime.UtcNow.AddMonths(CertificateFactory.DefaultLifeTime - 1));
|
||||
certificate.NotAfter.ToUniversalTime().ShouldBeLessThan(DateTime.UtcNow.AddMonths(CertificateFactory.DefaultLifeTime).AddDays(2));
|
||||
}
|
||||
var originalKeyHash = SHA256.HashData(await File.ReadAllBytesAsync(privatePath, ct));
|
||||
(current.CertificateManager as IDisposable)?.Dispose(); current = null;
|
||||
current = await factory.CreateAsync(settings, ct);
|
||||
using var reused = X509CertificateLoader.LoadCertificateFromFile(certPath);
|
||||
reused.Thumbprint.ShouldBe(certificate.Thumbprint);
|
||||
SHA256.HashData(await File.ReadAllBytesAsync(privatePath, ct)).ShouldBe(originalKeyHash,
|
||||
"reinitializing with a shorter creation default must not rotate an existing certificate or private key");
|
||||
Directory.GetFiles(Path.Combine(root, "own", "certs"), "*.der").Length.ShouldBe(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
(current?.CertificateManager as IDisposable)?.Dispose();
|
||||
if (Directory.Exists(root)) Directory.Delete(root, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SecurityMode.None)]
|
||||
[InlineData(SecurityMode.SignAndEncrypt)]
|
||||
|
||||
Reference in New Issue
Block a user