runtime: More granular X.509 certificate loader

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

About this issue

  • Original URL
  • State: open
  • Created 10 months ago
  • Reactions: 5
  • Comments: 18 (18 by maintainers)

Most upvoted comments

why don’t ignore private keys by default?

99.99% of the time someone wants them, that’s why they have a PFX instead of a cert. The other 0.01% is that someone just wants to inspect a PFX to say what’s in it, and not load (or even decrypt) private keys.

Video

  • Added [UnsupportedOSPlatform("browser")] to match the current X509Certificate2 ctors.
  • Made all the string password parameters be string? password because that’s more correct
  • Added “FromFile” to the file-loading ones
  • We cut the “Load into” collection methods, we can always add them back later.
  • We should also obsolete the collection Import methods
namespace System.Security.Cryptography.X509Certificates
{
    [UnsupportedOSPlatform("browser")]
    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 LoadCertificateFromFile(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 LoadPkcs12FromFile(
            string path,
            string password,
            X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
            Pkcs12LoaderLimits? loaderLimits = null);
        public static partial X509Certificate2 LoadPkcs12FromFile(
            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 LoadPkcs12CollectionFromFile(
            string path,
            string? password,
            X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet,
            Pkcs12LoaderLimits loaderLimits = null);
        public static partial X509Certificate2Collection LoadPkcs12CollectionFromFile(
            string path,
            ReadOnlySpan<char> password,
            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,
            MaxCertificates = 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.")
        {
        }
    }

    // .NET 9+
    public partial class X509Certificate2
    {
        // mark all byte[] and fileName ctors as [Obsolete]
    }

        // .NET 9+
    public partial class X509Certificate2Collection
    {
        // mark all byte[] and fileName Import methods as [Obsolete]
    }
}

I’d also skip … all Windows specific stuff to be honest.

Yeah, that was all cut in the first review session, along with Pkcs7 (since you can just use SignedCms for that).

I’m trying to think of a case where you’d actually need all those knobs on the limiter in real life scenario

Generally, one wouldn’t. 95% of callers will want the defaults, 1% will want DangerousNoLimits, 4% will want to toggle one of the preserve or ignore options, and the rest is mainly so that if we ever feel justified in ratcheting a default tighter that someone can undo us breaking a file that used to work.

with some arbitrary unit which user doesn’t need to concern themselves about

That’s pretty hard to document. The numbers in the limiter correspond to numbers in the spec. So at least someone somewhere can have Opinions and tie them in to things in RFCs.

I’d imagine the shape of APIs look somewhat similar to…

Unless I’ve missed something, you don’t collect a password for Pkcs12. And I think that the flags enum for what formats are supported is a pit of failure… people will specify .Any because it always works, and then they may as well use the current API.

DER = 1 << 4, PEM = 1 << 5

@vcsjones and I had chatted about PEM-or-DER, and decided it was too annoying for most people to want to have to deal with as separate methods.

I was in the process of saying it might be helpful on API that are part of a protocol that expect only PEM or DER(BER) in context, but I think that’s only true if the API rejects extraneous data (which we said it won’t) – trailing data for BER/DER, or anything other than whitespace outside the PEM encapsulation boundary for PEM.

My intent is to say that’s where we want to go, and if the meeting doesn’t immediately agree then defer that to a later issue. Unless you’re thinking it will be a hot debate in the issue, vs the review meeting?

No, the meeting - I just don’t want to block this getting approved on sorting the deprecation.