// #800 Windows service gate probe. Peer auto-accept is always false. // prepare [applicationName] create the client application certificate // endpoints list advertised endpoints; fail if None or anonymous is offered // probe expected = allow | key-deny | peer-deny | anon-deny // The API key comes from GATE_PROBE_KEY (never an argument). One JSON line is printed per run. using System.Text; using System.Text.Json; using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var ct = deadline.Token; var mode = args[0]; var root = Path.GetFullPath(args[1]); var telemetry = DefaultTelemetry.Create(_ => { }); var clientName = mode == "prepare" && args.Length > 2 ? args[2] : "Gate800Client"; var security = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "own"), SubjectName = "CN=" + clientName }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "issuer") }, TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "trusted") }, RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(root, "rejected") }, AutoAcceptUntrustedCertificates = false, }; var configuration = new ApplicationConfiguration(telemetry) { ApplicationName = clientName, ApplicationUri = "urn:localhost:" + clientName, ApplicationType = ApplicationType.Client, SecurityConfiguration = security, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 }, }; await using var application = new ApplicationInstance(telemetry) { ApplicationName = configuration.ApplicationName, ApplicationType = ApplicationType.Client, ApplicationConfiguration = configuration }; await configuration.ValidateAsync(ApplicationType.Client, ct); await application.CheckApplicationInstanceCertificatesAsync(true, lifeTimeInMonths: 12, ct); if (mode == "prepare") { Emit(new { mode, result = "prepared", clientName, autoAccept = security.AutoAcceptUntrustedCertificates }); return 0; } var url = args[2]; List advertised; using (var discovery = await DiscoveryClient.CreateAsync(configuration, new Uri(url), DiagnosticsMasks.None, ct)) advertised = (await discovery.GetEndpointsAsync(default, ct)).ToList(); var summary = advertised.Select(e => new { mode = e.SecurityMode.ToString(), policy = (e.SecurityPolicyUri ?? "").Split('#').Last(), tokens = TokenTypes(e), certSha1 = e.ServerCertificate.Length > 0 ? Convert.ToHexString(System.Security.Cryptography.SHA1.HashData(e.ServerCertificate.ToArray())) : "", }).ToList(); if (mode == "endpoints") { var insecure = summary.Any(e => e.mode == nameof(MessageSecurityMode.None)); var anonymous = summary.Any(e => e.tokens.Contains(nameof(UserTokenType.Anonymous))); Emit(new { mode, result = insecure || anonymous ? "insecure-offered" : "secure-only", endpoints = summary }); return insecure || anonymous ? 3 : 0; } var wanted = args[3] switch { "sign" => MessageSecurityMode.Sign, "none" => MessageSecurityMode.None, _ => MessageSecurityMode.SignAndEncrypt }; var wantedPolicy = wanted == MessageSecurityMode.None ? SecurityPolicies.None : SecurityPolicies.Basic256Sha256; var expected = args[4]; var selected = advertised.FirstOrDefault(e => e.SecurityMode == wanted && e.SecurityPolicyUri == wantedPolicy) ?? throw new InvalidOperationException("No " + wantedPolicy + " " + wanted + " endpoint advertised."); var identity = expected == "anon-deny" ? new UserIdentity() : new UserIdentity("apikey", Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("GATE_PROBE_KEY") ?? throw new InvalidOperationException("Missing GATE_PROBE_KEY."))); try { using var session = await new DefaultSessionFactory(telemetry).CreateAsync(configuration, new ConfiguredEndpoint(null, selected, EndpointConfiguration.Create(configuration)), false, true, "Gate800Probe", 60_000, identity, default, ct); if (expected != "allow") { Emit(new { mode, securityMode = wanted.ToString(), expected, result = "UNEXPECTED-ADMIT" }); await session.CloseAsync(ct); return 4; } var uns = session.NamespaceUris.GetIndex("https://zb.com/otopcua/uns"); var reads = new[] { new ReadValueId { NodeId = VariableIds.Server_ServerStatus_State, AttributeId = Attributes.Value }, new ReadValueId { NodeId = VariableIds.Server_ServiceLevel, AttributeId = Attributes.Value }, new ReadValueId { NodeId = new NodeId("EQ-000000000800/GateValue", (ushort)Math.Max(uns, 0)), AttributeId = Attributes.Value }, }; // The timer-driven virtual tag needs a moment after boot; retry its read for up to 20 s. List? results = null; for (var attempt = 0; attempt < 20; attempt++) { var response = await session.ReadAsync(null, 0, TimestampsToReturn.Both, reads, ct); results = response.Results.ToList(); if (uns >= 0 && StatusCode.IsGood(results[2].StatusCode)) break; await Task.Delay(1000, ct); } var state = results![0]; var level = results[1]; var vt = results[2]; var ok = StatusCode.IsGood(state.StatusCode) && StatusCode.IsGood(level.StatusCode); Emit(new { mode, securityMode = wanted.ToString(), policy = wantedPolicy.Split('#').Last(), expected, result = ok ? "allowed" : "READ-FAILED", serverState = $"{state.Value} ({state.StatusCode})", serviceLevel = $"{level.Value} ({level.StatusCode})", unsNamespaceIndex = uns, virtualTag = $"{vt.Value} ({vt.StatusCode})", serverCertSha1 = summary.First(e => e.mode == wanted.ToString()).certSha1, autoAccept = false, }); await session.CloseAsync(ct); return ok ? 0 : 5; } catch (ServiceResultException error) when (expected != "allow") { var accepted = expected switch { "key-deny" => new[] { StatusCodes.BadUserAccessDenied, StatusCodes.BadIdentityTokenRejected, StatusCodes.BadIdentityTokenInvalid }, "anon-deny" => new[] { StatusCodes.BadUserAccessDenied, StatusCodes.BadIdentityTokenRejected, StatusCodes.BadIdentityTokenInvalid, StatusCodes.BadSecurityPolicyRejected }, _ => new[] { StatusCodes.BadCertificateUntrusted, StatusCodes.BadSecurityChecksFailed, StatusCodes.BadSecureChannelClosed }, }; var good = accepted.Contains(error.StatusCode); Emit(new { mode, securityMode = wanted.ToString(), expected, result = good ? "denied" : "DENIED-WITH-UNEXPECTED-CODE", status = error.StatusCode.ToString(), autoAccept = false }); return good ? 0 : 6; } static string[] TokenTypes(EndpointDescription e) { var types = new List(); foreach (var t in e.UserIdentityTokens) types.Add(t.TokenType.ToString()); return types.Distinct().ToArray(); } static void Emit(object value) => Console.WriteLine(JsonSerializer.Serialize(value));