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.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";
 
        // 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);
                    }
                }
            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);
        }
    }
 
    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);
    }
}