Skip to content

More granular X.509 certificate loader #91763

Closed
@bartonjs

Description

@bartonjs

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]
    }
}

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions