diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs index bbd33ff..9fa8640 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs @@ -146,9 +146,19 @@ public sealed class SelfSignedCertificateProvider HardenPermissions(temp); // Writing into an existing file truncates content but preserves its ACL/mode. - File.WriteAllBytes(temp, pfx); - File.Move(temp, path, overwrite: true); - HardenPermissions(path); + // If the write or move fails the hardened temp file (which may contain private-key + // material) must not be left on disk; delete it best-effort before rethrowing. + try + { + File.WriteAllBytes(temp, pfx); + File.Move(temp, path, overwrite: true); + HardenPermissions(path); + } + catch (Exception) + { + try { File.Delete(temp); } catch { /* best effort */ } + throw; + } X509Certificate2 loaded = X509CertificateLoader.LoadPkcs12FromFile(path, password: null, KeyStorageFlags()); Log("Generated", loaded); diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs index e75375e..7b8dfc8 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs @@ -116,6 +116,37 @@ public sealed class SelfSignedCertificateProviderTests () => CreateProvider(options, new FakeTimeProvider()).LoadOrCreate()); } + /// + /// Verifies that GenerateAndPersist cleans up the hardened .tmp file when persist fails. + /// The failure is induced by setting SelfSignedCertPath to a path whose parent directory + /// is an existing regular file, causing Directory.CreateDirectory (or the subsequent write) + /// to throw an IOException/UnauthorizedAccessException. + /// + [Fact] + public void LoadOrCreate_DeletesTempFile_WhenPersistFails() + { + string outerDir = Directory.CreateTempSubdirectory().FullName; + try + { + // Create a regular file at what would be the parent directory of the cert path. + // Any attempt to create that "directory" or write files into it must fail. + string fileActingAsDir = Path.Combine(outerDir, "notadir"); + File.WriteAllText(fileActingAsDir, "block"); + + // Point the cert path inside the regular file — Directory.CreateDirectory will + // throw because the parent path component is a file, not a directory. + string certPath = Path.Combine(fileActingAsDir, "gw.pfx"); + string expectedTemp = certPath + ".tmp"; + + TlsOptions options = new() { SelfSignedCertPath = certPath }; + Assert.ThrowsAny(() => CreateProvider(options, new FakeTimeProvider()).LoadOrCreate()); + + // The .tmp file must not be left behind. + Assert.False(File.Exists(expectedTemp), $"Leaked temp file: {expectedTemp}"); + } + finally { Directory.Delete(outerDir, recursive: true); } + } + private const string SubjectAltNameOid = "2.5.29.17"; private static string ReadSubjectAltNames(X509Certificate2 cert)