120 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|