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.
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:
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}");
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
openssl pkcs12 -in server.pfx -passin pass:pfx-password -noout -info 2>&1MAC: 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
X509CertificateLoaderrefuses more than 300,000 unless you raise itsPkcs12LoaderLimits, as above; other readers may have limits of their own. A lowerMacIterationsinPkcs12WriteOptionsmakes 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 passrequireMac: 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.