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)