fix(gateway): delete temp cert file on persist failure
Wrap the WriteAllBytes/Move/HardenPermissions sequence in a try/catch so that any failure best-effort deletes the hardened .tmp file (which may already hold PFX/private-key bytes) before rethrowing. Add a test that induces a persist failure by pointing SelfSignedCertPath inside a regular file and asserts no .tmp is left on disk.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -116,6 +116,37 @@ public sealed class SelfSignedCertificateProviderTests
|
||||
() => CreateProvider(options, new FakeTimeProvider()).LoadOrCreate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<Exception>(() => 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)
|
||||
|
||||
Reference in New Issue
Block a user