using System.Security.Cryptography; using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); // P-256 on Windows, Linux and macOS // a new file (never overwrite a key), readable by its owner only: mode 600var options = new FileStreamOptions { Mode = FileMode.CreateNew, Access = FileAccess.Write };if (!OperatingSystem.IsWindows()) // Windows protects files with ACLs instead{ options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;}using (var writer = new StreamWriter("key.pem", options)){ writer.WriteLine(key.ExportPkcs8PrivateKeyPem()); // "BEGIN PRIVATE KEY"}File.WriteAllText("pub.pem", key.ExportSubjectPublicKeyInfoPem() + "\n"); // "BEGIN PUBLIC KEY"Console.Write(File.ReadAllText("pub.pem")); // "BEGIN ENCRYPTED PRIVATE KEY": PKCS#8 under PBES2 (AES-256-CBC, PBKDF2-HMAC-SHA256)string password = Environment.GetEnvironmentVariable("KEY_PASSWORD")!;var pbe = new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, iterationCount: 600_000);File.WriteAllText("key-encrypted.pem", key.ExportEncryptedPkcs8PrivateKeyPem(password, pbe) + "\n"); // reading it backusing var reloaded = ECDsa.Create();reloaded.ImportFromEncryptedPem(File.ReadAllText("key-encrypted.pem"), password); How it works
ECDsa.Create(ECCurve.NamedCurves.nistP256)generates the key immediately;nistP384andnistP521select the larger NIST curves.ExportPkcs8PrivateKeyPem()returnsBEGIN PRIVATE KEYtext andExportSubjectPublicKeyInfoPem()returnsBEGIN PUBLIC KEY; the run checks withopenssl pkeythat the two files belong together.FileMode.CreateNewrefuses to overwrite an existing key, and on Linux and macOSUnixCreateModecreates the file with mode 600 before any key material is written.ExportEncryptedPkcs8PrivateKeyPemwraps the key in PBES2 with the chosen cipher, hash and PBKDF2 iteration count;ImportFromEncryptedPemreverses it, and OpenSSL opens the same file with-passin.
Gotchas
- The PEM export methods return text without a trailing newline. Tools that concatenate PEM files, and
cmpagainst OpenSSL output, need the final\nadded as above. - On Linux, .NET delegates to the system OpenSSL, so curve support follows the platform: friendly names like
"P-256"throwPlatformNotSupportedExceptionthere, while theECCurve.NamedCurvesmembers work everywhere. UnixCreateModethrows on Windows, hence theOperatingSystem.IsWindows()check — which also keeps the platform analyzer (CA1416) quiet. On Windows, restrict the file with ACLs or keep keys in the certificate store.ECDsaobjects wrap native handles. Withoutusing, private key material stays in unmanaged memory until the finalizer runs.