using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; using ZB.MOM.WW.OtOpcUa.Core.Authorization; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache; using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(45)); var ct = deadline.Token; var mode = args[0]; var root = Path.GetFullPath(args[1]); if (mode == "seed") { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["LocalDb:Path"] = Path.Combine(root, "localdb.db") }).Build(); await using var provider = new ServiceCollection().AddLogging().AddOtOpcUaLocalDb(config).BuildServiceProvider(); var presented = Environment.GetEnvironmentVariable("SMOKE_API_KEY") ?? throw new InvalidOperationException("Missing task credential."); if (!ApiKeySecret.TryParse(presented, out var id, out var secret)) throw new InvalidOperationException("Invalid task credential format."); var pepper = Environment.GetEnvironmentVariable("Security__ApiKeys__Pepper") ?? throw new InvalidOperationException("Missing task pepper."); var artifact = JsonSerializer.SerializeToUtf8Bytes(new { Clusters = new[] { new { ClusterId = "smoke", Name = "Smoke" } }, RawFolders = new[] { new { RawFolderId = "smoke", Name = "Smoke", ClusterId = "smoke" } }, ApiKeys = new[] { new { ApiKeyId = id, Name = "publish-smoke", Principal = "publish-smoke", Enabled = true, SecretHash = ApiKeySecret.ComputeHash(secret, pepper), CreatedUtc = DateTime.UtcNow } }, NodeAcls = new[] { new { NodeAclId = "smoke-read", ClusterId = "smoke", Principal = "publish-smoke", ScopeKind = 0, ScopeId = (string?)null, PermissionFlags = 7 } }, }); var revision = Convert.ToHexStringLower(SHA256.HashData(artifact)); await provider.GetRequiredService().StoreAsync("smoke", ZB.MOM.WW.OtOpcUa.Commons.Types.DeploymentId.NewId().ToString(), revision, artifact, ct); Console.WriteLine(JsonSerializer.Serialize(new { mode, result = "seeded-through-public-cache", revision })); return; } var telemetry = DefaultTelemetry.Create(_ => { }); var clientName = mode == "prepare-cli" ? "OtOpcUaClient" : "PublishSmokeClient"; 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 is "prepare" or "prepare-cli") { Console.WriteLine(JsonSerializer.Serialize(new { mode, result = "prepared", autoAccept = security.AutoAcceptUntrustedCertificates })); return; } var endpoint = args[2]; var expected = args[3]; var credential = Environment.GetEnvironmentVariable("SMOKE_API_KEY") ?? throw new InvalidOperationException("Missing task credential."); try { var selected = await CoreClientUtils.SelectEndpointAsync(configuration, endpoint, true, telemetry, ct) ?? throw new InvalidOperationException("No secure endpoint advertised."); if (selected.SecurityMode != MessageSecurityMode.SignAndEncrypt || selected.SecurityPolicyUri != SecurityPolicies.Basic256Sha256) throw new InvalidOperationException("Unexpected negotiated security policy."); using var session = await new DefaultSessionFactory(telemetry).CreateAsync(configuration, new ConfiguredEndpoint(null, selected, EndpointConfiguration.Create(configuration)), false, true, "PublishSmoke", 60_000, new UserIdentity("apikey", Encoding.UTF8.GetBytes(credential)), default, ct); if (expected != "allow") throw new InvalidOperationException("A denied test credential or peer was admitted."); var response = await session.ReadAsync(null, 0, TimestampsToReturn.Both, [new ReadValueId { NodeId = VariableIds.Server_ServerStatus_State, AttributeId = Attributes.Value }], ct); if (response.Results.Count != 1 || StatusCode.IsBad(response.Results[0].StatusCode)) throw new InvalidOperationException("Server status read failed."); Console.WriteLine(JsonSerializer.Serialize(new { mode, expected, result = "allowed", policy = selected.SecurityPolicyUri, securityMode = selected.SecurityMode.ToString(), status = response.Results[0].StatusCode.ToString(), autoAccept = false })); await session.CloseAsync(ct); } catch (ServiceResultException error) when (expected != "allow") { var accepted = expected == "key-deny" ? new[] { StatusCodes.BadUserAccessDenied, StatusCodes.BadIdentityTokenRejected, StatusCodes.BadIdentityTokenInvalid } : new[] { StatusCodes.BadCertificateUntrusted, StatusCodes.BadSecurityChecksFailed, StatusCodes.BadSecureChannelClosed }; if (!accepted.Contains(error.StatusCode)) throw; Console.WriteLine(JsonSerializer.Serialize(new { mode, expected, result = "denied", status = error.StatusCode.ToString(), autoAccept = false })); }