Description
Currently, loading a certificate from memory or a file is performed by the X509Certificate2
constructors or the X509Certificate2Collection.Import
methods. These existing routines support many different formats (for single certificates: X509Certificate, PKCS#7 SignedCms, Windows Serialized Certificate, Authenticode-signed assets, and PKCS#12/PFX; for collections: X509Certificate, PKCS#7 SignedCms (interpreted differently than the single certificate case), Windows Serialized Store, and PKCS#12/PFX). Since many of those formats themselves support multiple encodings (e.g. X509Certificate-PEM and X509Certificate-DER), these members are very complicated.
While sometimes convenient to a caller, the design has proven lacking in multiple ways:
- When a protocol or file format indicates the presence of a certificate,
new X509Certificate2(data)
will unexpectedly allow several other file formats, making the most obvious code load data that other systems correctly reject as invalid. - Of all the file formats these member support, only PKCS#12/PFX requires more options. These options are ignored when the input data/file is not a PKCS#12/PFX, leading to user confusion.
- PKCS#12/PFX requires more options... but the overloads that do not accept those options will provide defaults. Since PKCS#12/PFX is the only file format supported by these members that can also load private keys into memory, it isn't possible to understand the full security implications of
new X509Certificate2(bytes)
. - PKCS#12/PFX is a very complicated format which can be very expensive to load. Many .NET users have expressed desire for some control knobs to limit the total amount of work attempted.
- Authenticode-signed assets, Windows Serialized Certificates, and Windows Serialized Stores are only supported on Windows, but there's no way to mark that with
[SupportedOS]
This proposal puts loader methods on a new type, both to avoid "do I want the ctor or the static?" but also so that this type can be made available to .NET Standard 2.0/.NET Framework.
The expected packaging is inbox for .NET 9+, and Microsoft.Bcl.Cryptography for .NET Standard 2.0/.NET Framework/.NET 8-.
namespace System.Security.Cryptography.X509Certificates
{
public static partial class X509CertificateLoader
{
// A single X509Certificate value, PEM or DER
// No collection variant needed.
public static partial X509Certificate2 LoadCertificate(byte[] data);
public static partial X509Certificate2 LoadCertificate(ReadOnlySpan<byte> data);
public static partial X509Certificate2 LoadCertificate(string path);
// Load "the best" certificate from a PFX: first-cert-with-privkey ?? first-cert ?? throw.
// equivalent to the certificate from `new X509Certificate2(data, password, keyStorageFlags)`
public static X509Certificate2 LoadPkcs12(
byte[] data,
string password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static partial X509Certificate2 LoadPkcs12(
ReadOnlySpan<byte> data,
ReadOnlySpan<char> password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static X509Certificate2 LoadPkcs12(
string path,
string password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static partial X509Certificate2 LoadPkcs12(
string path,
ReadOnlySpan<char> password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
// Load a PFX as a collection.
// null loaderLimits means Pkcs12LoaderLimits.Default
public static X509Certificate2Collection LoadPkcs12Collection(
byte[] data,
string password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits loaderLimits = null);
public static partial X509Certificate2Collection LoadPkcs12Collection(
ReadOnlySpan<byte> data,
ReadOnlySpan<char> password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static X509Certificate2Collection LoadPkcs12Collection(
string path,
string password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits loaderLimits = null);
public static partial X509Certificate2Collection LoadPkcs12Collection(
string path,
ReadOnlySpan<char> password,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
// Add into an existing collection
// equivalent to `X509Certificate2Collection.Import(data, password, keyStorageFlags)`
public static X509Certificate2Collection LoadPkcs12Collection(
byte[] data,
string password,
X509Certificate2Collection collection,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static partial X509Certificate2Collection LoadPkcs12Collection(
ReadOnlySpan<byte> data,
ReadOnlySpan<char> password,
X509Certificate2Collection collection,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static X509Certificate2Collection LoadPkcs12Collection(
string path,
string password,
X509Certificate2Collection collection,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
public static partial X509Certificate2Collection LoadPkcs12Collection(
string path,
ReadOnlySpan<char> password,
X509Certificate2Collection collection,
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
Pkcs12LoaderLimits? loaderLimits = null);
}
public sealed class Pkcs12LoaderLimits
{
public static Pkcs12LoaderLimits Defaults { get; } = new Pkcs12LoaderLimits();
public static Pkcs12LoaderLimits DangerousNoLimits { get; } = new Pkcs12LoaderLimits
{
MacIterationLimit = null,
IndividualKdfIterationLimit = null,
TotalKdfIterationLimit = null,
MaxKeys = null,
MaxCerts = null,
PreserveStorageProvider = true,
PreserveKeyName = true,
PreserveCertificateAlias = true,
PreserveUnknownAttributes = true,
};
public Pkcs12LoaderLimits();
public Pkcs12LoaderLimits(Pkcs12LoaderLimits copyFrom);
public bool IsReadOnly { get; }
public void MakeReadOnly();
public int? MacIterationLimit { get; set; } = 300_000;
public int? IndividualKdfIterationLimit { get; set; } = 300_000;
public int? TotalKdfIterationLimit { get; set; } = 1_000_000;
public int? MaxKeys { get; set; } = 200;
public int? MaxCertificates { get; set; } = 200;
public bool PreserveStorageProvider { get; set; } // = false;
public bool PreserveKeyName { get; set; } // = false;
public bool PreserveCertificateAlias { get; set; } // = false;
public bool PreserveUnknownAttributes { get; set; } // = false;
public bool IgnorePrivateKeys { get; set; } // = false;
public bool IgnoreEncryptedAuthSafes { get; set; } // = false;
}
public sealed class Pkcs12LoadLimitExceededException : CryptographicException
{
public Pkcs12LoadLimitExceededException(string propertyName)
: base($"The PKCS#12/PFX violated the '{propertyName}' limit.")
{
}
private Pkcs12LoadLimitExceededException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
// .NET 9+
public partial class X509Certificate2
{
// mark all byte[] and fileName ctors as [Obsolete]
}
}