using System.Net; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using Opc.Ua; using Opc.Ua.Configuration; using Opc.Ua.Identity; using Opc.Ua.Server; using Opc.Ua.Server.Hosting; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters; /// /// An in-process OPC Foundation exposing None, Sign and /// SignAndEncrypt (Basic256Sha256) endpoints with STRICT client trust /// (AutoAcceptUntrustedCertificates = false) and its own temp PKI — the Gitea #121 /// counterpart for real mutual-trust tests of RealOpcUaClient. /// internal sealed class SecureOpcUaTestServer : IAsyncDisposable { private readonly ApplicationInstance _app; private readonly string _pkiRoot; private SecureOpcUaTestServer(ApplicationInstance app, string pkiRoot, string url, X509Certificate2 certificate) { _app = app; _pkiRoot = pkiRoot; Url = url; Certificate = certificate; } /// The endpoint URL (localhost, so the server certificate's domain matches). public string Url { get; } /// The server's application certificate (public part). public X509Certificate2 Certificate { get; } /// Starts a server that trusts exactly . /// Client certificates the server's trusted peer store holds at start. /// The running server. public static Task StartAsync(params X509Certificate2[] trustedClients) => StartCoreAsync(apiKey: null, trustedClients); /// /// Starts a server that also admits the UserName apikey with exactly /// as its password (ScadaBridge#125), and refuses any other password /// with BadIdentityTokenRejected once it has decrypted the token. The UserName token /// policy is Basic256Sha256, so the password is encrypted on every channel, None included. /// /// The one password the server accepts. /// Client certificates the server's trusted peer store holds at start. /// The running server. public static Task StartWithApiKeyAsync(string apiKey, params X509Certificate2[] trustedClients) => StartCoreAsync(apiKey, trustedClients); private static async Task StartCoreAsync(string? apiKey, X509Certificate2[] trustedClients) { var pkiRoot = Path.Combine(Path.GetTempPath(), "sb121-server", Guid.NewGuid().ToString("N")); var port = FreePort(); var url = $"opc.tcp://localhost:{port}/sb121"; // Trust is written BEFORE start, so no test depends on trust-list cache refresh timing. var trustedCerts = Path.Combine(pkiRoot, "trusted", "certs"); Directory.CreateDirectory(trustedCerts); foreach (var c in trustedClients) await File.WriteAllBytesAsync(Path.Combine(trustedCerts, $"{c.Thumbprint}.der"), c.RawData); var telemetry = DefaultTelemetry.Create(_ => { }); var config = new ApplicationConfiguration(telemetry) { ApplicationName = "SB121TestServer", ApplicationUri = $"urn:localhost:SB121TestServer:{port}", ApplicationType = ApplicationType.Server, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pkiRoot, "own"), SubjectName = "CN=SB121TestServer", CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType, }, TrustedPeerCertificates = new CertificateTrustList { StorePath = Path.Combine(pkiRoot, "trusted") }, TrustedIssuerCertificates = new CertificateTrustList { StorePath = Path.Combine(pkiRoot, "issuers") }, RejectedCertificateStore = new CertificateTrustList { StorePath = Path.Combine(pkiRoot, "rejected") }, AutoAcceptUntrustedCertificates = false, AddAppCertToTrustedStore = false, }, TransportQuotas = new TransportQuotas { OperationTimeout = 15_000 }, ServerConfiguration = new ServerConfiguration { BaseAddresses = [url], SecurityPolicies = [ new ServerSecurityPolicy { SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None }, new ServerSecurityPolicy { SecurityMode = MessageSecurityMode.Sign, SecurityPolicyUri = SecurityPolicies.Basic256Sha256 }, new ServerSecurityPolicy { SecurityMode = MessageSecurityMode.SignAndEncrypt, SecurityPolicyUri = SecurityPolicies.Basic256Sha256 }, ], UserTokenPolicies = apiKey is null ? [new UserTokenPolicy(UserTokenType.Anonymous)] : [new UserTokenPolicy(UserTokenType.Anonymous), new UserTokenPolicy(UserTokenType.UserName) { SecurityPolicyUri = SecurityPolicies.Basic256Sha256 }], }, }; await config.ValidateAsync(ApplicationType.Server); var app = new ApplicationInstance(config, telemetry) { ApplicationName = config.ApplicationName, ApplicationType = ApplicationType.Server, }; await app.CheckApplicationInstanceCertificatesAsync(silent: true, lifeTimeInMonths: 12); await app.StartAsync(new TestServer(telemetry, apiKey)); // SDK 2: the loaded application certificate lives in the certificate manager, not on the identifier. using var entry = config.CertificateManager.AcquireApplicationCertificateByType( ObjectTypeIds.RsaSha256ApplicationCertificateType) ?? throw new InvalidOperationException("The test server has no application certificate."); var cert = X509CertificateLoader.LoadCertificate(entry.Certificate.RawData); return new SecureOpcUaTestServer(app, pkiRoot, url, cert); } /// public async ValueTask DisposeAsync() { try { await _app.StopAsync(); } catch { /* best-effort */ } Certificate.Dispose(); try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort */ } } /// A that registers the API-key authenticator before it starts. private sealed class TestServer : StandardServer { public TestServer(ITelemetryContext telemetry, string? apiKey) : base(telemetry) { if (apiKey is not null) AddPreStartupTask(new RegisterAuthenticator(new ApiKeyAuthenticator(apiKey))); } } private sealed class RegisterAuthenticator(IUserTokenAuthenticator authenticator) : IServerPreStartupTask { public ValueTask OnServerStartingAsync(IServerContext server, CancellationToken cancellationToken = default) { ((IServerInternal)server).IdentityRegistry.Register(authenticator); return ValueTask.CompletedTask; } } private sealed class ApiKeyAuthenticator(string apiKey) : IUserTokenAuthenticator { public UserTokenType TokenType => UserTokenType.UserName; public string? IssuedTokenProfileUri => null; public ValueTask AuthenticateAsync(AuthenticationContext context, CancellationToken ct = default) { if (context.TokenHandler is not UserNameIdentityTokenHandler userName) return ValueTask.FromResult(AuthenticationResult.NotHandled); var accepted = userName.UserName == "apikey" && userName.DecryptedPassword is { } password && System.Text.Encoding.UTF8.GetString(password) == apiKey; return ValueTask.FromResult(accepted ? AuthenticationResult.Accept(new UserIdentity(userName)) : AuthenticationResult.Reject(new ServiceResult(StatusCodes.BadIdentityTokenRejected))); } } private static int FreePort() { var l = new TcpListener(IPAddress.Loopback, 0); l.Start(); var port = ((IPEndPoint)l.LocalEndpoint).Port; l.Stop(); return port; } }