using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Transport; using ZB.MOM.WW.ScadaBridge.Transport.Signing; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Configuration; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// ScadaBridge#110 decision 4: the bundle signing private key is supplied as a /// ${secret:} reference, expanded by the same pre-host /// Program.cs runs over the whole configuration. Proves the expanded multi-line PEM binds into /// , passes startup validation and signs a verifiable bundle manifest. /// public sealed class TransportSigningSecretExpansionTests { private sealed class DictResolver(Dictionary secrets) : ISecretResolver { public Task GetAsync(SecretName name, CancellationToken ct) => Task.FromResult(secrets.TryGetValue(name.Value, out var v) ? v : null); } /// A ${secret:} PrivateKey expands, validates and signs; the config file never held the key. [Fact] public async Task Secret_reference_private_key_expands_validates_and_signs() { using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); var privatePem = key.ExportPkcs8PrivateKeyPem(); var publicPem = key.ExportSubjectPublicKeyInfoPem(); var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["ScadaBridge:Transport:SourceEnvironment"] = "dev", ["ScadaBridge:Transport:Signing:Enabled"] = "true", ["ScadaBridge:Transport:Signing:PrivateKey"] = "${secret:bundle-signing-key}", }).Build(); await new SecretReferenceExpander(new DictResolver(new() { ["bundle-signing-key"] = privatePem })) .ExpandConfigurationAsync(configuration, default); var options = new TransportOptions(); configuration.GetSection("ScadaBridge:Transport").Bind(options); Assert.Equal(privatePem, options.Signing.PrivateKey); Assert.False(new TransportOptionsValidator().Validate(null, options).Failed); options.Signing.TrustedKeys.Add(new TrustedBundleSigningKey { Name = "dev", PublicKey = publicPem }); var o = Options.Create(options); var signer = new BundleSigner(new ConfiguredBundleSigningKeySource(o), new ConfiguredBundleTrustStore(o), o); var manifest = Encoding.UTF8.GetBytes("{\"m\":1}"); var verdict = signer.Verify(manifest, signer.Sign(manifest)); Assert.Equal(ZB.MOM.WW.ScadaBridge.Commons.Types.Transport.BundleSignatureStatus.Verified, verdict.Status); } /// An unresolved ${secret:} reference must not silently become an unparseable literal that boots. [Fact] public async Task Unresolvable_secret_reference_fails_expansion() { var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["ScadaBridge:Transport:Signing:PrivateKey"] = "${secret:missing-key}", }).Build(); await Assert.ThrowsAnyAsync(() => new SecretReferenceExpander(new DictResolver(new())).ExpandConfigurationAsync(configuration, default)); } }