Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/Sdk2CertificateValidationTests.cs
T

120 lines
6.3 KiB
C#

using System.Security.Cryptography.X509Certificates;
using Opc.Ua;
using Opc.Ua.Configuration;
using Opc.Ua.Security.Certificates;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
/// <summary>Exercises the target certificate manager with real issuer stores and signed certificates.</summary>
public sealed class Sdk2CertificateValidationTests
{
[Theory]
[InlineData("valid")]
[InlineData("expired")]
[InlineData("revoked")]
[InlineData("missing-issuer")]
[InlineData("bad-issuer-usage")]
public async Task Trust_and_certificate_failures_keep_the_configured_rejection_policy(string scenario)
{
var ct = TestContext.Current.CancellationToken;
await using var pki = await ValidationPki.CreateAsync(scenario, ct);
var result = await pki.Configuration.CertificateManager.ValidateAsync(pki.Leaf, ct: ct);
if (scenario == "valid") result.IsValid.ShouldBeTrue(result.ToString());
else
{
result.IsValid.ShouldBeFalse();
var expected = scenario switch
{
"expired" => StatusCodes.BadCertificateTimeInvalid,
"revoked" => StatusCodes.BadCertificateRevoked,
"missing-issuer" => StatusCodes.BadCertificateChainIncomplete,
_ => StatusCodes.BadCertificateIssuerUseNotAllowed,
};
(result.StatusCode == expected || result.Errors.Any(e => e.StatusCode == expected)).ShouldBeTrue(
$"Expected {expected}; actual {result.StatusCode}: {string.Join(", ", result.Errors)}");
}
pki.Configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates.ShouldBeFalse();
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task A_trusted_chain_does_not_bypass_endpoint_identity_checks(bool wrongUri)
{
await using var pki = await ValidationPki.CreateAsync("valid", TestContext.Current.CancellationToken);
(await pki.Configuration.CertificateManager.ValidateAsync(pki.Leaf, ct: TestContext.Current.CancellationToken))
.IsValid.ShouldBeTrue();
var endpoint = new ConfiguredEndpoint(null, new EndpointDescription
{
EndpointUrl = wrongUri ? "opc.tcp://localhost:4840" : "opc.tcp://other.invalid:4840",
Server = new ApplicationDescription { ApplicationUri = wrongUri ? "urn:wrong-application" : ValidationPki.PeerUri },
}, EndpointConfiguration.Create(pki.Configuration));
var error = Should.Throw<ServiceResultException>(() =>
{
if (wrongUri) pki.Configuration.CertificateManager.ValidateApplicationUri(pki.Leaf, endpoint);
else pki.Configuration.CertificateManager.ValidateDomains(pki.Leaf, endpoint);
});
error.StatusCode.ShouldBe(wrongUri ? StatusCodes.BadCertificateUriInvalid : StatusCodes.BadCertificateHostNameInvalid);
}
private sealed class ValidationPki : IAsyncDisposable
{
internal const string PeerUri = "urn:Sdk2ValidationPeer";
private readonly string _root = Path.Combine(Path.GetTempPath(), $"otua-validation-{Guid.NewGuid():N}");
private ApplicationInstance? _application;
private Certificate? _issuer;
internal Certificate Leaf { get; private set; } = null!;
internal ApplicationConfiguration Configuration { get; private set; } = null!;
internal static async Task<ValidationPki> CreateAsync(string scenario, CancellationToken ct)
{
var pki = new ValidationPki();
try
{
var now = DateTime.UtcNow;
var issuerBuilder = CertificateBuilder.Create("CN=Sdk2ValidationIssuer")
.SetNotBefore(now.AddDays(-10)).SetNotAfter(now.AddDays(365)).SetCAConstraint(-1);
if (scenario == "bad-issuer-usage")
issuerBuilder.AddExtension(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true));
pki._issuer = issuerBuilder.SetRSAKeySize(2048).CreateForRSA();
var names = new SubjectAlternativeNameBuilder();
names.AddUri(new Uri(PeerUri)); names.AddDnsName("localhost");
pki.Leaf = CertificateBuilder.Create("CN=Sdk2ValidationPeer")
.SetNotBefore(now.AddDays(-2)).SetNotAfter(scenario == "expired" ? now.AddDays(-1) : now.AddDays(30))
.AddExtension(names.Build()).SetIssuer(pki._issuer).SetRSAKeySize(2048).CreateForRSA();
if (scenario != "missing-issuer")
{
var certs = Path.Combine(pki._root, "trusted", "certs"); Directory.CreateDirectory(certs);
await File.WriteAllBytesAsync(Path.Combine(certs, "issuer.der"), pki._issuer.RawData, ct);
if (scenario == "revoked")
{
var crls = Path.Combine(pki._root, "trusted", "crl"); Directory.CreateDirectory(crls);
var crl = new X509CRL(CrlBuilder.Create(pki._issuer.SubjectName).AddRevokedCertificate(pki.Leaf).CreateForRSA(pki._issuer));
await File.WriteAllBytesAsync(Path.Combine(crls, "issuer.crl"), crl.RawData, ct);
}
}
var telemetry = DefaultTelemetry.Create(_ => { });
var security = TestClientSecurity.Build(pki._root); security.AutoAcceptUntrustedCertificates = false;
pki.Configuration = new ApplicationConfiguration(telemetry)
{
ApplicationName = "Sdk2Validation", ApplicationUri = "urn:Sdk2Validation", ApplicationType = ApplicationType.Client,
SecurityConfiguration = security, ClientConfiguration = new ClientConfiguration(),
};
pki._application = new ApplicationInstance(telemetry) { ApplicationConfiguration = pki.Configuration };
await pki.Configuration.ValidateAsync(ApplicationType.Client, ct);
return pki;
}
catch { await pki.DisposeAsync(); throw; }
}
public async ValueTask DisposeAsync()
{
if (_application is not null) await _application.DisposeAsync();
Leaf?.Dispose(); _issuer?.Dispose();
if (Directory.Exists(_root)) Directory.Delete(_root, true);
}
}
}