File: Internal\Certificates\CertificateConfigLoader.cs
Web Access
Project: src\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
 
internal sealed class CertificateConfigLoader : ICertificateConfigLoader
{
    public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger<KestrelServer> logger)
    {
        HostEnvironment = hostEnvironment;
        Logger = logger;
    }
 
    public IHostEnvironment HostEnvironment { get; }
    public ILogger<KestrelServer> Logger { get; }
 
    public bool IsTestMock => false;
 
    public (X509Certificate2?, X509Certificate2Collection?) LoadCertificate(CertificateConfig? certInfo, string endpointName)
    {
        if (certInfo is null)
        {
            return (null, null);
        }
 
        if (certInfo.IsFileCert && certInfo.IsStoreCert)
        {
            throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
        }
        else if (certInfo.IsFileCert)
        {
            var certificatePath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path!);
            var fullChain = new X509Certificate2Collection();
            fullChain.ImportFromPemFile(certificatePath);
 
            if (certInfo.KeyPath != null)
            {
                var certificateKeyPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.KeyPath);
                var certificate = GetCertificate(certificatePath);
 
                if (certificate != null)
                {
                    certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
                }
                else
                {
                    Logger.FailedToLoadCertificate(certificateKeyPath);
                }
 
                if (certificate != null)
                {
                    if (OperatingSystem.IsWindows())
                    {
                        return (PersistKey(certificate), fullChain);
                    }
 
                    return (certificate, fullChain);
                }
                else
                {
                    Logger.FailedToLoadCertificateKey(certificateKeyPath);
                }
 
                throw new InvalidOperationException(CoreStrings.InvalidPemKey);
            }
 
            return (new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path!), certInfo.Password), fullChain);
        }
        else if (certInfo.IsStoreCert)
        {
            return (LoadFromStoreCert(certInfo), null);
        }
 
        return (null, null);
    }
 
    private static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
    {
        // We need to force the key to be persisted.
        // See https://github.com/dotnet/runtime/issues/23749
        var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
        return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
    }
 
    private static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string? password)
    {
        // OIDs for the certificate key types.
        const string RSAOid = "1.2.840.113549.1.1.1";
        const string DSAOid = "1.2.840.10040.4.1";
        const string ECDsaOid = "1.2.840.10045.2.1";
 
        const string MLDsa44Oid = "2.16.840.1.101.3.4.3.17";
        const string MLDsa65Oid = "2.16.840.1.101.3.4.3.18";
        const string MLDsa87Oid = "2.16.840.1.101.3.4.3.19";
 
        const string SlhDsaSha2_128sOid = "2.16.840.1.101.3.4.3.20";
        const string SlhDsaSha2_128fOid = "2.16.840.1.101.3.4.3.21";
        const string SlhDsaSha2_192sOid = "2.16.840.1.101.3.4.3.22";
        const string SlhDsaSha2_192fOid = "2.16.840.1.101.3.4.3.23";
        const string SlhDsaSha2_256sOid = "2.16.840.1.101.3.4.3.24";
        const string SlhDsaSha2_256fOid = "2.16.840.1.101.3.4.3.25";
        const string SlhDsaShake_128sOid = "2.16.840.1.101.3.4.3.26";
        const string SlhDsaShake_128fOid = "2.16.840.1.101.3.4.3.27";
        const string SlhDsaShake_192sOid = "2.16.840.1.101.3.4.3.28";
        const string SlhDsaShake_192fOid = "2.16.840.1.101.3.4.3.29";
        const string SlhDsaShake_256sOid = "2.16.840.1.101.3.4.3.30";
        const string SlhDsaShake_256fOid = "2.16.840.1.101.3.4.3.31";
 
        const string MLDsa44WithRSA2048PssPreHashSha256Oid = "2.16.840.1.114027.80.9.1.0";
        const string MLDsa44WithRSA2048Pkcs15PreHashSha256Oid = "2.16.840.1.114027.80.9.1.1";
        const string MLDsa44WithEd25519PreHashSha512Oid = "2.16.840.1.114027.80.9.1.2";
        const string MLDsa44WithECDsaP256PreHashSha256Oid = "2.16.840.1.114027.80.9.1.3";
        const string MLDsa65WithRSA3072PssPreHashSha512Oid = "2.16.840.1.114027.80.9.1.4";
        const string MLDsa65WithRSA3072Pkcs15PreHashSha512Oid = "2.16.840.1.114027.80.9.1.5";
        const string MLDsa65WithRSA4096PssPreHashSha512Oid = "2.16.840.1.114027.80.9.1.6";
        const string MLDsa65WithRSA4096Pkcs15PreHashSha512Oid = "2.16.840.1.114027.80.9.1.7";
        const string MLDsa65WithECDsaP256PreHashSha512Oid = "2.16.840.1.114027.80.9.1.8";
        const string MLDsa65WithECDsaP384PreHashSha512Oid = "2.16.840.1.114027.80.9.1.9";
        const string MLDsa65WithECDsaBrainpoolP256r1PreHashSha512Oid = "2.16.840.1.114027.80.9.1.10";
        const string MLDsa65WithEd25519PreHashSha512Oid = "2.16.840.1.114027.80.9.1.11";
        const string MLDsa87WithECDsaP384PreHashSha512Oid = "2.16.840.1.114027.80.9.1.12";
        const string MLDsa87WithECDsaBrainpoolP384r1PreHashSha512Oid = "2.16.840.1.114027.80.9.1.13";
        const string MLDsa87WithEd448PreHashShake256_512Oid = "2.16.840.1.114027.80.9.1.14";
        const string MLDsa87WithRSA3072PssPreHashSha512Oid = "2.16.840.1.114027.80.9.1.15";
        const string MLDsa87WithRSA4096PssPreHashSha512Oid = "2.16.840.1.114027.80.9.1.16";
        const string MLDsa87WithECDsaP521PreHashSha512Oid = "2.16.840.1.114027.80.9.1.17";
 
        // Duplication is required here because there are separate CopyWithPrivateKey methods for each algorithm.
        var keyText = File.ReadAllText(keyPath);
        switch (certificate.PublicKey.Oid.Value)
        {
            case RSAOid:
                {
                    using var rsa = RSA.Create();
                    ImportKeyFromFile(rsa, keyText, password);
 
                    try
                    {
                        return certificate.CopyWithPrivateKey(rsa);
                    }
                    catch (Exception ex)
                    {
                        throw CreateErrorGettingPrivateKeyException(keyPath, ex);
                    }
                }
            case ECDsaOid:
                {
                    using var ecdsa = ECDsa.Create();
                    ImportKeyFromFile(ecdsa, keyText, password);
 
                    try
                    {
                        return certificate.CopyWithPrivateKey(ecdsa);
                    }
                    catch (Exception ex)
                    {
                        throw CreateErrorGettingPrivateKeyException(keyPath, ex);
                    }
                }
            case DSAOid:
                {
                    using var dsa = DSA.Create();
                    ImportKeyFromFile(dsa, keyText, password);
 
                    try
                    {
                        return certificate.CopyWithPrivateKey(dsa);
                    }
                    catch (Exception ex)
                    {
                        throw CreateErrorGettingPrivateKeyException(keyPath, ex);
                    }
                }
            case MLDsa44Oid:
            case MLDsa65Oid:
            case MLDsa87Oid:
                {
#pragma warning disable SYSLIB5006 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
                    using var mlDsa = ImportMLDsaKeyFromFile(keyText, password);
 
                    try
                    {
                        return certificate.CopyWithPrivateKey(mlDsa);
                    }
                    catch (Exception ex)
                    {
                        throw CreateErrorGettingPrivateKeyException(keyPath, ex);
                    }
                }
            case SlhDsaSha2_128sOid:
            case SlhDsaSha2_128fOid:
            case SlhDsaSha2_192sOid:
            case SlhDsaSha2_192fOid:
            case SlhDsaSha2_256sOid:
            case SlhDsaSha2_256fOid:
            case SlhDsaShake_128sOid:
            case SlhDsaShake_128fOid:
            case SlhDsaShake_192sOid:
            case SlhDsaShake_192fOid:
            case SlhDsaShake_256sOid:
            case SlhDsaShake_256fOid:
                {
                    using var slhDsa = ImportSlhDsaKeyFromFile(keyText, password);
 
                    try
                    {
                        return certificate.CopyWithPrivateKey(slhDsa);
                    }
                    catch (Exception ex)
                    {
                        throw CreateErrorGettingPrivateKeyException(keyPath, ex);
                    }
                }
            case MLDsa44WithRSA2048PssPreHashSha256Oid:
            case MLDsa44WithRSA2048Pkcs15PreHashSha256Oid:
            case MLDsa44WithEd25519PreHashSha512Oid:
            case MLDsa44WithECDsaP256PreHashSha256Oid:
            case MLDsa65WithRSA3072PssPreHashSha512Oid:
            case MLDsa65WithRSA3072Pkcs15PreHashSha512Oid:
            case MLDsa65WithRSA4096PssPreHashSha512Oid:
            case MLDsa65WithRSA4096Pkcs15PreHashSha512Oid:
            case MLDsa65WithECDsaP256PreHashSha512Oid:
            case MLDsa65WithECDsaP384PreHashSha512Oid:
            case MLDsa65WithECDsaBrainpoolP256r1PreHashSha512Oid:
            case MLDsa65WithEd25519PreHashSha512Oid:
            case MLDsa87WithECDsaP384PreHashSha512Oid:
            case MLDsa87WithECDsaBrainpoolP384r1PreHashSha512Oid:
            case MLDsa87WithEd448PreHashShake256_512Oid:
            case MLDsa87WithRSA3072PssPreHashSha512Oid:
            case MLDsa87WithRSA4096PssPreHashSha512Oid:
            case MLDsa87WithECDsaP521PreHashSha512Oid:
                {
                    using var compositeMLDsa = ImportCompositeMLDsaKeyFromFile(keyText, password);
 
                    try
                    {
                        return certificate.CopyWithPrivateKey(compositeMLDsa);
                    }
                    catch (Exception ex)
                    {
                        throw CreateErrorGettingPrivateKeyException(keyPath, ex);
                    }
                }
#pragma warning restore SYSLIB5006 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
            default:
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, CoreStrings.UnrecognizedCertificateKeyOid, certificate.PublicKey.Oid.Value));
        }
    }
 
    private static InvalidOperationException CreateErrorGettingPrivateKeyException(string keyPath, Exception ex)
    {
        return new InvalidOperationException($"Error getting private key from '{keyPath}'.", ex);
    }
 
    private static X509Certificate2? GetCertificate(string certificatePath)
    {
        if (X509Certificate2.GetCertContentType(certificatePath) == X509ContentType.Cert)
        {
            return new X509Certificate2(certificatePath);
        }
 
        return null;
    }
 
    private static void ImportKeyFromFile(AsymmetricAlgorithm asymmetricAlgorithm, string keyText, string? password)
    {
        if (password == null)
        {
            asymmetricAlgorithm.ImportFromPem(keyText);
        }
        else
        {
            asymmetricAlgorithm.ImportFromEncryptedPem(keyText, password);
        }
    }
 
    [Experimental("SYSLIB5006")]
    private static MLDsa ImportMLDsaKeyFromFile(string keyText, string? password)
    {
        if (password == null)
        {
            return MLDsa.ImportFromPem(keyText);
        }
        else
        {
            return MLDsa.ImportFromEncryptedPem(keyText, password);
        }
    }
 
    [Experimental("SYSLIB5006")]
    private static SlhDsa ImportSlhDsaKeyFromFile(string keyText, string? password)
    {
        if (password == null)
        {
            return SlhDsa.ImportFromPem(keyText);
        }
        else
        {
            return SlhDsa.ImportFromEncryptedPem(keyText, password);
        }
    }
 
    [Experimental("SYSLIB5006")]
    private static CompositeMLDsa ImportCompositeMLDsaKeyFromFile(string keyText, string? password)
    {
        if (password == null)
        {
            return CompositeMLDsa.ImportFromPem(keyText);
        }
        else
        {
            return CompositeMLDsa.ImportFromEncryptedPem(keyText, password);
        }
    }
 
    private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
    {
        var subject = certInfo.Subject!;
        var storeName = string.IsNullOrEmpty(certInfo.Store) ? StoreName.My.ToString() : certInfo.Store;
        var location = certInfo.Location;
        var storeLocation = StoreLocation.CurrentUser;
        if (!string.IsNullOrEmpty(location))
        {
            storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true);
        }
        var allowInvalid = certInfo.AllowInvalid ?? false;
 
        return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid);
    }
}