Skip to content

mTLS intermediate certificate handling for application that is both server and client #121199

@hlinkaintenscz

Description

@hlinkaintenscz

Due to specification, we have application, which is a node in a system where all nodes can communicate with each other, and each node can be both client and server.

The communication requires mTLS and the certificate chain is 3 levels deep. CA is shared, but each node has its own subCA and leaf certificate. Each node has to provide leaf and intermediate (subCA) when communicating with the other side. The last requirement causes the headache.

When configuring server part, we can specify the chain easily (according to docs, ServerCertificateChain should not contain the leaf):

public static void ConfigureMutualTlsServer(this ListenOptions listenOptions, MutualTlsOptions mTlsOptions)
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var serverCert = mTlsOptions.GetLeafCert();
            var caCert = mTlsOptions.GetCaCert();
            var intermediateCert = mTlsOptions.GetIntermediateCert();

            httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
            httpsOptions.ServerCertificate = serverCert;
            httpsOptions.ServerCertificateChain = [intermediateCert];
            httpsOptions.CheckCertificateRevocation = false;
            httpsOptions.ClientCertificateValidation = (cert, chain, _) => cert.Validate(caCert, chain);
            httpsOptions.SslProtocols = SslProtocols.None;
        });
    }

the code above correctly sends both server cert and intermediate to the client.

The problem is that in order to include the intermediate certificate on the client's side, the only working option I found is to add the certificate to X509Store and the system will pick it up:

public static HttpMessageHandler ConfigureMutualTlsClient(this IServiceProvider _, MutualTlsOptions mTlsOptions)
    {
        var clientCert = mTlsOptions.GetLeafCert();

        var caCert = mTlsOptions.GetCaCert();
        
        RegisterIntermediateCertForClientAuth(mTlsOptions);

        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(clientCert);
        handler.CheckCertificateRevocationList = false;
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        handler.ServerCertificateCustomValidationCallback = (_, cert, chain, _) => cert.Validate(caCert, chain);
        handler.SslProtocols = SslProtocols.None;
        return handler;
    }

private static void RegisterIntermediateCertForClientAuth(MutualTlsOptions mTlsOptions)
    {
        if (mTlsOptions.IntermediateCertFileName == null)
            return;

        var certPath = Path.Combine(mTlsOptions.IntermediateCertPath!, mTlsOptions.IntermediateCertFileName);
        if (!File.Exists(certPath))
            throw new FileNotFoundException("Intermediate cert file not found", certPath);

        var intermediateCert = new X509Certificate2(certPath);
        using var intermediateStore = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser);
        intermediateStore.Open(OpenFlags.ReadWrite);

        var foundIntermediateCerts = intermediateStore.Certificates
            .Find(X509FindType.FindByThumbprint, intermediateCert.Thumbprint, validOnly: false);
        if (foundIntermediateCerts.Count == 0)
            intermediateStore.Add(intermediateCert);
    }

The problem is that placing the certificate into the store will affect the server side as well, causing httpsOptions.ServerCertificateChain = [intermediateCert!]; to be ignored and the server will end up sending the certificate found in the store.

Please note instances of MutualTlsOptions in client and server side are different objects, leading to different certificates. The application runs on .NET 8.0 on linux

#47680 (comment) was used as hint when trying to make the client send intermediate cert
#55368 another similar case, also suggesting that using intermediate store is the only way for client
however, both solutions also affect the server side of the application

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions