From d9fc02a8fbce310e5cb4fb8564eb64fd40824a74 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 24 Jul 2024 16:49:11 -0700 Subject: [PATCH 1/8] Create directories with secure permissions If we're creating it, make it 700. If it already exists, warn if it's not 700. --- .../CertificateManager.cs | 6 ++- .../MacOSCertificateManager.cs | 28 +++++++++++-- .../UnixCertificateManager.cs | 20 +++++++++ .../WindowsCertificateManager.cs | 41 +++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index eb16e6f51b54..313de9f57ea0 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -484,7 +484,9 @@ public void CleanupHttpsCertificates() protected abstract IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation); - internal static void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format) + protected abstract void CreateDirectoryWithPermissions(string directoryPath); + + internal void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format) { if (Log.IsEnabled()) { @@ -500,7 +502,7 @@ internal static void ExportCertificate(X509Certificate2 certificate, string path if (!string.IsNullOrEmpty(targetDirectoryPath)) { Log.CreateExportCertificateDirectory(targetDirectoryPath); - Directory.CreateDirectory(targetDirectoryPath); + CreateDirectoryWithPermissions(targetDirectoryPath); } byte[] bytes; diff --git a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs index b8c16b50c9a4..e89e76f432a9 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation; /// internal sealed class MacOSCertificateManager : CertificateManager { + private const UnixFileMode DirectoryPermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; + // User keychain. Guard with quotes when using in command lines since users may have set // their user profile (HOME) directory to a non-standard path that includes whitespace. private static readonly string MacOSUserKeychain = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Library/Keychains/login.keychain-db"; @@ -93,6 +95,7 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 publicCertif var tmpFile = Path.GetTempFileName(); try { + // We can't guarantee that the temp file is in a directory with sensible permissions, but we're not exporting the private key ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pfx); if (Log.IsEnabled()) { @@ -134,9 +137,7 @@ internal override void CorrectCertificateState(X509Certificate2 candidate) { try { - // Ensure that the directory exists before writing to the file. - Directory.CreateDirectory(MacOSUserHttpsCertificateLocation); - + // This path is in a well-known folder, so we trust the permissions. var certificatePath = GetCertificateFilePath(candidate); ExportCertificate(candidate, certificatePath, includePrivateKey: true, null, CertificateKeyExportFormat.Pfx); } @@ -152,6 +153,7 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) var tmpFile = Path.GetTempFileName(); try { + // We can't guarantee that the temp file is in a directory with sensible permissions, but we're not exporting the private key ExportCertificate(certificate, tmpFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); using var checkTrustProcess = Process.Start(new ProcessStartInfo( @@ -316,7 +318,7 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi } // Ensure that the directory exists before writing to the file. - Directory.CreateDirectory(MacOSUserHttpsCertificateLocation); + CreateDirectoryWithPermissions(MacOSUserHttpsCertificateLocation); File.WriteAllBytes(GetCertificateFilePath(certificate), certBytes); } @@ -474,4 +476,22 @@ protected override void RemoveCertificateFromUserStoreCore(X509Certificate2 cert RemoveCertificateFromKeychain(MacOSUserKeychain, certificate); } } + + protected override void CreateDirectoryWithPermissions(string directoryPath) + { +#pragma warning disable CA1416 // Validate platform compatibility (not supported on Windows) + var dirInfo = new DirectoryInfo(directoryPath); + if (dirInfo.Exists) + { + if ((dirInfo.UnixFileMode & ~DirectoryPermissions) != 0) + { + // TODO (acasey): Log.DirectoryPermissionsNotSecure(dirInfo.FullName); + } + } + else + { + Directory.CreateDirectory(directoryPath, DirectoryPermissions); + } +#pragma warning restore CA1416 // Validate platform compatibility + } } diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 6fa154bb8e93..e69fd54f482e 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation; /// internal sealed partial class UnixCertificateManager : CertificateManager { + private const UnixFileMode DirectoryPermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; + /// The name of an environment variable consumed by OpenSSL to locate certificates. private const string OpenSslCertificateDirectoryVariableName = "SSL_CERT_DIR"; @@ -449,6 +451,24 @@ protected override IList GetCertificatesToRemove(StoreName sto return ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, requireExportable: false); } + protected override void CreateDirectoryWithPermissions(string directoryPath) + { +#pragma warning disable CA1416 // Validate platform compatibility (not supported on Windows) + var dirInfo = new DirectoryInfo(directoryPath); + if (dirInfo.Exists) + { + if ((dirInfo.UnixFileMode & ~DirectoryPermissions) != 0) + { + // TODO (acasey): Log.DirectoryPermissionsNotSecure(dirInfo.FullName); + } + } + else + { + Directory.CreateDirectory(directoryPath, DirectoryPermissions); + } +#pragma warning restore CA1416 // Validate platform compatibility + } + private static string GetChromiumNssDb(string homeDirectory) { return Path.Combine(homeDirectory, ".pki", "nssdb"); diff --git a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs index 058fb7fef023..657045f433ce 100644 --- a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs +++ b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs @@ -3,10 +3,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Runtime.Versioning; +using System.Security.AccessControl; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; namespace Microsoft.AspNetCore.Certificates.Generation; @@ -126,4 +130,41 @@ protected override IList GetCertificatesToRemove(StoreName sto { return ListCertificates(storeName, storeLocation, isValid: false); } + + protected override void CreateDirectoryWithPermissions(string directoryPath) + { + var dirInfo = new DirectoryInfo(directoryPath); + + if (!dirInfo.Exists) + { + // We trust the default permissions on Windows enough not to apply custom ACLs. + // We'll warn below if things seem really off. + dirInfo.Create(); + } + + var currentUser = WindowsIdentity.GetCurrent(); + var currentUserSid = currentUser.User; + var systemSid = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, domainSid: null); + var adminGroupSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null); + + var dirSecurity = dirInfo.GetAccessControl(); + var accessRules = dirSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); + + foreach (FileSystemAccessRule rule in accessRules) + { + var idRef = rule.IdentityReference; + if (rule.AccessControlType == AccessControlType.Allow && + !idRef.Equals(currentUserSid) && + !idRef.Equals(systemSid) && + !idRef.Equals(adminGroupSid)) + { + // This is just a heuristic - determining whether the cumulative effect of the rules + // is to allow access to anyone other than the current user, system, or administrators + // is very complicated. We're not going to do anything but log, so an approximation + // is fine. + // TODO (acasey): Log.DirectoryPermissionsNotSecure(dirInfo.FullName); + break; + } + } + } } From 2f114423dc88c1b110e555613ad1f00a2548b57e Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 24 Jul 2024 16:58:30 -0700 Subject: [PATCH 2/8] Don't create a directory specified by the user --- .../Shared/DevelopmentCertificate.cs | 2 +- .../CertificateGeneration/CertificateManager.cs | 15 +++++++++++++++ .../MacOSCertificateManager.cs | 2 +- .../UnixCertificateManager.cs | 2 +- .../WindowsCertificateManager.cs | 4 +--- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ProjectTemplates/Shared/DevelopmentCertificate.cs b/src/ProjectTemplates/Shared/DevelopmentCertificate.cs index e9c8ea6fa169..080f35c3b3f8 100644 --- a/src/ProjectTemplates/Shared/DevelopmentCertificate.cs +++ b/src/ProjectTemplates/Shared/DevelopmentCertificate.cs @@ -35,7 +35,7 @@ private static string EnsureDevelopmentCertificates(string certificatePath, stri var manager = CertificateManager.Instance; var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1)); var certificateThumbprint = certificate.Thumbprint; - CertificateManager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx); + manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx); return certificateThumbprint; } diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 313de9f57ea0..ef40de6412c0 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -323,6 +323,14 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( { try { + // If the user specified a non-existent directory, we don't want to be responsible + // for setting the permissions appropriately, so we'll bail. + var exportDir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(exportDir) && !Directory.Exists(exportDir)) + { + throw new InvalidOperationException($"The directory '{exportDir}' does not exist."); + } + ExportCertificate(certificate, path, includePrivateKey, password, keyExportFormat); } catch (Exception e) @@ -486,6 +494,10 @@ public void CleanupHttpsCertificates() protected abstract void CreateDirectoryWithPermissions(string directoryPath); + /// + /// Will create directories to make it possible to write to . + /// If you don't want that, check for existence before calling this method. + /// internal void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format) { if (Log.IsEnabled()) @@ -1232,6 +1244,9 @@ public sealed class CertificateManagerEventSource : EventSource [Event(111, Level = EventLevel.LogAlways, Message = "For OpenSSL trust to take effect, '{0}' must be listed in the {2} environment variable. " + "See https://aka.ms/dev-certs-trust for more information.")] internal void UnixSuggestSettingEnvironmentVariableWithoutExample(string certDir, string envVarName) => WriteEvent(111, certDir, envVarName); + + [Event(112, Level = EventLevel.Warning, Message = "Directory '{0}' may be readable by other users.")] + internal void DirectoryPermissionsNotSecure(string directoryPath) => WriteEvent(112, directoryPath); } internal sealed class UserCancelledTrustException : Exception diff --git a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs index e89e76f432a9..a38e22762190 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -485,7 +485,7 @@ protected override void CreateDirectoryWithPermissions(string directoryPath) { if ((dirInfo.UnixFileMode & ~DirectoryPermissions) != 0) { - // TODO (acasey): Log.DirectoryPermissionsNotSecure(dirInfo.FullName); + Log.DirectoryPermissionsNotSecure(dirInfo.FullName); } } else diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index e69fd54f482e..3a86cb545035 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -459,7 +459,7 @@ protected override void CreateDirectoryWithPermissions(string directoryPath) { if ((dirInfo.UnixFileMode & ~DirectoryPermissions) != 0) { - // TODO (acasey): Log.DirectoryPermissionsNotSecure(dirInfo.FullName); + Log.DirectoryPermissionsNotSecure(dirInfo.FullName); } } else diff --git a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs index 657045f433ce..85a72be37c66 100644 --- a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs +++ b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.Versioning; using System.Security.AccessControl; @@ -162,7 +160,7 @@ protected override void CreateDirectoryWithPermissions(string directoryPath) // is to allow access to anyone other than the current user, system, or administrators // is very complicated. We're not going to do anything but log, so an approximation // is fine. - // TODO (acasey): Log.DirectoryPermissionsNotSecure(dirInfo.FullName); + Log.DirectoryPermissionsNotSecure(dirInfo.FullName); break; } } From febccb6cfc6795e80382a19856e977f798161638 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 25 Jul 2024 14:15:51 -0700 Subject: [PATCH 3/8] Validate error on export to non-existent directory --- .../test/CertificateManagerTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index e6c81b006a56..6896a81ea55e 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -352,6 +352,29 @@ public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutPass Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); } + [ConditionalFact] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")] + public void EnsureCreateHttpsCertificate_CannotExportToNonExistentDirectory() + { + // Arrange + const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pem"; + + _fixture.CleanupCertificates(); + + var now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false); + Output.WriteLine(creation.ToString()); + ListCertificates(); + + // Act + var result = _manager + .EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), Path.Combine("NoSuchDirectory", CertificateName)); + + // Assert + Assert.Equal(EnsureCertificateResult.ErrorExportingTheCertificate, result); + } + [Fact] public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsIncorrect() { From dffa0f78b45e06503ea008b72acae7e4b845ee30 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Mon, 29 Jul 2024 11:11:54 -0700 Subject: [PATCH 4/8] Add clarifications based on PR feedback --- .../CertificateGeneration/UnixCertificateManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 3a86cb545035..96f0c8ec8ca7 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -76,7 +76,8 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) Log.UnixNotTrustedByDotnet(); } - var nickname = GetCertificateNickname(certificate); + // Will become the name of the file on disk and the nickname in the NSS DBs + var certificateNickname = GetCertificateNickname(certificate); var sslCertDirString = Environment.GetEnvironmentVariable(OpenSslCertificateDirectoryVariableName); if (string.IsNullOrEmpty(sslCertDirString)) @@ -90,7 +91,7 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) var sslCertDirs = sslCertDirString.Split(Path.PathSeparator); foreach (var sslCertDir in sslCertDirs) { - var certPath = Path.Combine(sslCertDir, nickname + ".pem"); + var certPath = Path.Combine(sslCertDir, certificateNickname + ".pem"); if (File.Exists(certPath)) { var candidate = X509CertificateLoader.LoadCertificateFromFile(certPath); @@ -127,7 +128,7 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) { foreach (var nssDb in nssDbs) { - if (IsCertificateInNssDb(nickname, nssDb)) + if (IsCertificateInNssDb(certificateNickname, nssDb)) { sawTrustSuccess = true; } @@ -140,6 +141,7 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) } } + // Success & Failure => Partial; Success => Full; Failure => None return sawTrustSuccess ? sawTrustFailure ? TrustLevel.Partial From b6d17930fe64024cf912155f951730fef8076030 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Mon, 29 Jul 2024 11:14:31 -0700 Subject: [PATCH 5/8] Explain why the new ExportCertificate call is safe --- src/Shared/CertificateGeneration/UnixCertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 96f0c8ec8ca7..c099794df9a1 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -248,7 +248,7 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) if (needToExport) { // Security: we don't need the private key for trust, so we don't export it. - // Note that this will create directories as needed. + // Note that this will create directories as needed. We control `certPath`, so the permissions should be fine. ExportCertificate(certificate, certPath, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); } From 677d39dd7caa761e0190a5a9ff614b6ba8021df4 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 30 Jul 2024 14:56:55 -0700 Subject: [PATCH 6/8] Fix copy-paste error. --- src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 6896a81ea55e..879861ad1f1f 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -357,7 +357,7 @@ public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutPass public void EnsureCreateHttpsCertificate_CannotExportToNonExistentDirectory() { // Arrange - const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pem"; + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CannotExportToNonExistentDirectory) + ".pem"; _fixture.CleanupCertificates(); From de7fee2615ded069282ec0021be606bce6eec4b3 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 30 Jul 2024 14:59:47 -0700 Subject: [PATCH 7/8] Add clarifying comment --- src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 879861ad1f1f..09a32825e50e 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -368,6 +368,7 @@ public void EnsureCreateHttpsCertificate_CannotExportToNonExistentDirectory() ListCertificates(); // Act + // Export the certificate (same method, but this time with an output path) var result = _manager .EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), Path.Combine("NoSuchDirectory", CertificateName)); From 314b620cbef38e53555360dbdf228b790c27f362 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 30 Jul 2024 15:00:53 -0700 Subject: [PATCH 8/8] Make error message more helpful --- src/Shared/CertificateGeneration/CertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index ef40de6412c0..96e5630c43f1 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -328,7 +328,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( var exportDir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(exportDir) && !Directory.Exists(exportDir)) { - throw new InvalidOperationException($"The directory '{exportDir}' does not exist."); + throw new InvalidOperationException($"The directory '{exportDir}' does not exist. Choose permissions carefully when creating it."); } ExportCertificate(certificate, path, includePrivateKey, password, keyExportFormat);