Recursive AI

Convert PEM to PFX (PKCS#12) in C#

All onecrypto recipes

Load the certificate and key with Crypto.Load, then write both into one password-protected file with Pkcs12File.Create. The program below uses a certificate and key made by OpenSSL, and opens the result with OneCrypto, with .NET’s X509CertificateLoader and with OpenSSL.

Terminal
dotnet new console -o PemToPfx && cd PemToPfx
dotnet add package RecursiveAI.OneCrypto --version 0.1.0-preview.1
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 365 \
  -subj "/CN=api.example.com" -keyout key.pem -out cert.pem

OneCrypto is a preview, so dotnet add package needs --version or --prerelease. Put this in Program.cs and run dotnet run:

C#
using System.Security.Cryptography.X509Certificates;
using RecursiveAI.OneCrypto;
using RecursiveAI.OneCrypto.Pkcs;
 
using LoadedItems certFile = Crypto.Load("cert.pem");
using LoadedItems keyFile = Crypto.Load("key.pem");
Certificate cert = certFile.Certificates[0];
PrivateKeyInfo key = keyFile.PrivateKeys[0].Info;
 
// Defaults: AES-256-CBC, PBKDF2-HMAC-SHA-256 with 600,000 iterations, HMAC-SHA-256 MAC
Pkcs12File pfx = Pkcs12File.Create(key, cert, chain: [], "pfx-password",
    friendlyName: "api.example.com");
File.WriteAllBytes("server.pfx", pfx.Encoded.ToArray());
Console.WriteLine($"Wrote server.pfx: {pfx.Encoded.Length:N0} bytes, " +
    $"MAC {pfx.MacKind}");
 
// Read it back: the MAC is checked before anything is decrypted
using Pkcs12Contents contents = Pkcs12File.Parse(File.ReadAllBytes("server.pfx"))
    .Open("pfx-password");
Console.WriteLine($"OneCrypto: {contents.Certificates.Count} certificate " +
    $"({contents.Certificates[0].Subject}), {contents.PrivateKeys.Count} private key");
 
// .NET's own loader stops at 300,000 iterations by default: raise its limits
var limits = new Pkcs12LoaderLimits { MacIterationLimit = 600_000,
    IndividualKdfIterationLimit = 600_000, TotalKdfIterationLimit = 2_000_000 };
using X509Certificate2 x509 = X509CertificateLoader.LoadPkcs12FromFile("server.pfx",
    "pfx-password", loaderLimits: limits);
Console.WriteLine($".NET:      {x509.Subject}, has private key: {x509.HasPrivateKey}");
Text
Wrote server.pfx: 1,184 bytes, MAC Pkcs12
OneCrypto: 1 certificate (CN=api.example.com), 1 private key
.NET:      CN=api.example.com, has private key: True

Pass the intermediate certificates as chain so servers can send the whole chain. Pkcs12File.Create also takes a list of bags for files with several keys, and Pkcs12WriteOptions chooses the MAC (Pbmac1 for RFC 9579), the iteration counts and whether certificates are encrypted.

Check it with OpenSSL

Terminal
openssl pkcs12 -in server.pfx -passin pass:pfx-password -noout -info 2>&1
Text
MAC: sha256, Iteration 600000
MAC length: 32, salt length: 16
PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 600000, PRF hmacWithSHA256
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 600000, PRF hmacWithSHA256
Certificate bag

Limits

  • Not audited. This library has not been independently audited. It is a preview: 0.1.0-preview.1, and the API can change.
  • Iteration counts. The defaults use 600,000 PBKDF2 iterations for the key, the certificates and the MAC. .NET’s X509CertificateLoader refuses more than 300,000 unless you raise its Pkcs12LoaderLimits, as above; other readers may have limits of their own. A lower MacIterations in Pkcs12WriteOptions makes the password cheaper to guess. Triple DES and RC2 are never written; legacy PBEs are read, except RC4.
  • Limits on input. Files read from elsewhere are bounded: 64 MiB, 5,000,000 iterations, 256 safes and 4,096 bags by default (PkcsLimits). A file with no MAC is refused unless you pass requireMac: false.

How PKCS#8, PKCS#12 and OpenSSH keys are read and written: PKCS. To read any .pfx without knowing what it holds: Load a PEM or PKCS#8 private key.