114 lines
5.6 KiB
C#
114 lines
5.6 KiB
C#
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class OpcUaClientCertAuthTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies that BuildCertificateIdentity rejects missing certificate path.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildCertificateIdentity_rejects_missing_path()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Certificate };
|
|
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.BuildCertificateIdentity(opts))
|
|
.Message.ShouldContain("UserCertificatePath");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that BuildCertificateIdentity rejects nonexistent certificate file.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildCertificateIdentity_rejects_nonexistent_file()
|
|
{
|
|
var opts = new OpcUaClientDriverOptions
|
|
{
|
|
AuthType = OpcUaAuthType.Certificate,
|
|
UserCertificatePath = Path.Combine(Path.GetTempPath(), $"does-not-exist-{Guid.NewGuid():N}.pfx"),
|
|
};
|
|
Should.Throw<FileNotFoundException>(() => OpcUaClientDriver.BuildCertificateIdentity(opts));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that BuildCertificateIdentity loads a valid PFX with private key.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildCertificateIdentity_loads_a_valid_PFX_with_private_key()
|
|
{
|
|
// Generate a self-signed cert on the fly so the test doesn't ship a static PFX.
|
|
// The driver doesn't care about the issuer — just needs a cert with a private key.
|
|
using var rsa = RSA.Create(2048);
|
|
var req = new CertificateRequest("CN=OpcUaClientCertAuthTests", rsa,
|
|
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
|
|
|
var tmpPath = Path.Combine(Path.GetTempPath(), $"opcua-cert-test-{Guid.NewGuid():N}.pfx");
|
|
File.WriteAllBytes(tmpPath, cert.Export(X509ContentType.Pfx, "testpw"));
|
|
try
|
|
{
|
|
var opts = new OpcUaClientDriverOptions
|
|
{
|
|
AuthType = OpcUaAuthType.Certificate,
|
|
UserCertificatePath = tmpPath,
|
|
UserCertificatePassword = "testpw",
|
|
};
|
|
var identity = OpcUaClientDriver.BuildCertificateIdentity(opts);
|
|
identity.ShouldNotBeNull();
|
|
identity.TokenType.ShouldBe(Opc.Ua.UserTokenType.Certificate);
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(tmpPath); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
[Fact]
|
|
public async Task Certificate_identity_signs_repeatedly_and_requires_reopen_after_file_rotation()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"opcua-cert-rotation-{Guid.NewGuid():N}.pfx");
|
|
try
|
|
{
|
|
using var key = RSA.Create(2048);
|
|
var request = new CertificateRequest("CN=OpcUaSdk2Signing", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
using var original = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
|
File.WriteAllBytes(path, original.Export(X509ContentType.Pfx, "test-password"));
|
|
var options = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Certificate,
|
|
UserCertificatePath = path, UserCertificatePassword = "test-password" };
|
|
var identity = OpcUaClientDriver.BuildCertificateIdentity(options);
|
|
byte[] challenge = [1, 2, 3, 4];
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
var signature = await identity.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256,
|
|
TestContext.Current.CancellationToken);
|
|
using var publicKey = original.GetRSAPublicKey();
|
|
publicKey.ShouldNotBeNull().VerifyData(challenge, signature.Signature.Span,
|
|
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1).ShouldBeTrue();
|
|
}
|
|
using var cancelled = new CancellationTokenSource();
|
|
cancelled.Cancel();
|
|
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
|
await identity.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256, cancelled.Token));
|
|
|
|
using var rotatedKey = RSA.Create(2048);
|
|
var rotatedRequest = new CertificateRequest("CN=OpcUaSdk2Signing", rotatedKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
using var rotated = rotatedRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
|
File.WriteAllBytes(path, rotated.Export(X509ContentType.Pfx, "test-password"));
|
|
var error = await Should.ThrowAsync<Opc.Ua.ServiceResultException>(async () =>
|
|
await identity.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256,
|
|
TestContext.Current.CancellationToken));
|
|
error.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadIdentityTokenInvalid);
|
|
var fresh = OpcUaClientDriver.BuildCertificateIdentity(options);
|
|
var freshSignature = await fresh.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256,
|
|
TestContext.Current.CancellationToken);
|
|
using var rotatedPublicKey = rotated.GetRSAPublicKey();
|
|
rotatedPublicKey.ShouldNotBeNull().VerifyData(challenge, freshSignature.Signature.Span,
|
|
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1).ShouldBeTrue();
|
|
}
|
|
finally { File.Delete(path); }
|
|
}
|
|
|
|
}
|