using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Transport.Signing; namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Signing; /// ScadaBridge#110: production-boundary advisory, startup warning, and key-never-logged guarantees. public sealed class BundleSigningPolicyAdvisoryTests { private sealed class CapturingLogger : ILogger { public List<(LogLevel Level, string Message)> Entries { get; } = new(); public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => Entries.Add((logLevel, formatter(state, exception) + (exception?.ToString() ?? string.Empty))); } /// Advisory fires only for ProductionBoundary without RequireSignedBundles. [Theory] [InlineData(true, false, true)] [InlineData(true, true, false)] [InlineData(false, false, false)] [InlineData(false, true, false)] public void Evaluate_flags_only_a_production_boundary_that_accepts_unsigned( bool boundary, bool require, bool expectAdvisory) { var signing = new TransportSigningOptions { ProductionBoundary = boundary, RequireSignedBundles = require }; Assert.Equal(expectAdvisory, BundleSigningPolicyAdvisory.Evaluate(signing) is not null); } /// The startup service logs a Warning for the unmet expectation and stays silent otherwise. [Theory] [InlineData(true, false, 1)] [InlineData(true, true, 0)] [InlineData(false, false, 0)] public async Task Startup_service_warns_exactly_when_advisory_applies(bool boundary, bool require, int warnings) { var opts = new TransportOptions(); opts.Signing.ProductionBoundary = boundary; opts.Signing.RequireSignedBundles = require; var logger = new CapturingLogger(); await new BundleSigningPolicyStartupWarning(Options.Create(opts), logger).StartAsync(default); Assert.Equal(warnings, logger.Entries.Count(e => e.Level == LogLevel.Warning)); } /// The ProductionBoundary flag is bound from configuration and defaults off. [Fact] public void ProductionBoundary_defaults_off() { Assert.False(new TransportOptions().Signing.ProductionBoundary); Assert.False(new TransportOptions().Signing.RequireSignedBundles); } /// A malformed private key must fail validation without echoing the key text into the failure message. [Fact] public void Validation_failure_for_bad_private_key_does_not_echo_the_key() { const string marker = "SUPER-SECRET-KEY-MATERIAL-12345"; var opts = new TransportOptions { SourceEnvironment = "dev" }; opts.Signing.Enabled = true; opts.Signing.PrivateKey = "-----BEGIN PRIVATE KEY-----\n" + marker + "\n-----END PRIVATE KEY-----"; var result = new TransportOptionsValidator().Validate(null, opts); Assert.True(result.Failed); Assert.DoesNotContain(marker, string.Join("|", result.Failures)); } /// Signing never writes the private key (or its base64) into the produced signature entry. [Fact] public void Signature_entry_does_not_contain_private_key_material() { using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); var pem = key.ExportPkcs8PrivateKeyPem(); var der = Convert.ToBase64String(key.ExportPkcs8PrivateKey()); var sig = BundleSignerTests.Signer(BundleSignerTests.SigningOptions(pem)).Sign(new byte[] { 1, 2, 3 })!; var text = System.Text.Encoding.UTF8.GetString(sig); Assert.DoesNotContain(der, text); Assert.DoesNotContain("PRIVATE KEY", text); } }