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
{
///
/// Verifies that BuildCertificateIdentity rejects missing certificate path.
///
[Fact]
public void BuildCertificateIdentity_rejects_missing_path()
{
var opts = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Certificate };
Should.Throw(() => OpcUaClientDriver.BuildCertificateIdentity(opts))
.Message.ShouldContain("UserCertificatePath");
}
///
/// Verifies that BuildCertificateIdentity rejects nonexistent certificate file.
///
[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(() => OpcUaClientDriver.BuildCertificateIdentity(opts));
}
///
/// Verifies that BuildCertificateIdentity loads a valid PFX with private key.
///
[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(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(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); }
}
}