dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.18k stars 4.72k forks source link

Unable to get acceptableIssuers from LocalCertificateSelectionCallback #52499

Closed denisvasilik closed 2 years ago

denisvasilik commented 3 years ago

I'm using a mTLS setup and wanted to get a list of acceptableIssuers from the LocalCertificateSelectionCallback at the client application. This works great on Windows, but fails on Ubuntu 20.04. Here is a sample application I used for reproduction and a snippet of the relevant location:

private static X509Certificate SelectClientCertificate(
    object sender,
    string targetHost,
    X509CertificateCollection localCertificates,
    X509Certificate remoteCertificate,
    string[] acceptableIssuers)
{
    //
    // * Is only called once when running on Linux and does *not* provide
    //   acceptable issuers.
    //
    // * Is called twice when running on Windows and does provide
    //   acceptable issuers.
    //
    return localCertificates[0];
}

Configuration

Working configuration:

Errornous configuration:

Quick Analysis

During debugging I figured out that on Windows the method InitializeSecurityContext returns SecurityStatusPalErrorCode.CredentialsNeeded (when appropriate). As a consequence, the LocalCertificateSelectionCallback is called a second time with proper content of acceptable issuers. When looking at the InitializeSecurityContext or HandshakeInternal routine on Linux, it never returns SecurityStatusPalErrorCode.CredentialsNeeded. Instead it returns SecurityStatusPalErrorCode.ContinueNeeded which does not trigger LocalCertificateSelectionCallback. Hence, there's no second invocation of LocalCertificateSelectionCallback providing the acceptable issuers.

If this is a bug (and not a configuration issue) I would like to work on it in order to provide a fix.

ghost commented 3 years ago

Tagging subscribers to this area: @dotnet/ncl, @vcsjones See info in area-owners.md if you want to be subscribed.

Issue Details
I'm using a mTLS setup and wanted to get a list of `acceptableIssuers` from the `LocalCertificateSelectionCallback` at the client application. This works great on Windows, but fails on Ubuntu 20.04. Here is a [sample application](https://github.com/denisvasilik/dotnet-runtime-repro-mtls) I used for reproduction and a snippet of the relevant location: ``` private static X509Certificate SelectClientCertificate( object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) { // // * Is only called once when running on Linux and does *not* provide // acceptable issuers. // // * Is called twice when running on Windows and does provide // acceptable issuers. // return localCertificates[0]; } ``` ### Configuration Working configuration: - Window 10 - .NET 5.0.200 Errornous configuration: - Ubuntu 20.04 - .NET 5.0.201 ### Quick Analysis During debugging I figured out that on Windows the method [InitializeSecurityContext](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs#L83) returns `SecurityStatusPalErrorCode.CredentialsNeeded` (when appropriate). As a consequence, the `LocalCertificateSelectionCallback` is called a second time with proper content of acceptable issuers. When looking at the [InitializeSecurityContext](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs#L33) or [HandshakeInternal](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs#L99) routine on Linux, it never returns `SecurityStatusPalErrorCode.CredentialsNeeded`. Instead it returns `SecurityStatusPalErrorCode.ContinueNeeded` which does not trigger `LocalCertificateSelectionCallback`. Hence, there's no second invocation of `LocalCertificateSelectionCallback` providing the acceptable issuers. If this is a bug (and not a configuration issue) I would like to work on it in order to provide a fix.
Author: denisvasilik
Assignees: -
Labels: `area-System.Net.Security`, `untriaged`
Milestone: -
wfurt commented 3 years ago

What site are you connecting to @denisvasilik ? Does your peer provide list of CAs? Perhaps you can post packet capture of the handshake.

denisvasilik commented 3 years ago

For testing purposes I am trying to connect to mail.denisvasilik.com. In this case the server provides a Let's Encrypt server certificate and accepts a private PKI client certificate issued by denisvasilik-sa-root1-client1 or denisvasilik-ca-root1.

Sample application output on Windows

CN=denisvasilik-sa-root1-client1, O=denisvasilik, L=Munich, S=Bavaria, C=DE
CN=denisvasilik-ca-root1, O=denisvasilik, L=Munich, S=Bavaria, C=DE
Exception: No credentials are available in the security package

Note: I provided a dummy client certificate and key in the repository so it's not possible to finish the handshake successfully, but it's enough to retrieve the acceptable issuers.

Sample application output on Linux

Exception: No credentials are available in the security package

Here are no acceptableIssuers are provided.

I added a trace of the TLS handshake to the repro repository.

Thank you for your support, if you need further information just let me know.

wfurt commented 3 years ago

I check and it seems like the server sends two names:

Distinguished Name: (id-at-commonName=denisvasilik-sa-root1-client1,id-at-organizationName=denisvasilik,id-at-localityName=Munich,id-at-stateOrProvinceName=Bavaria,id-at-countryName=DE)
Distinguished Name: (id-at-commonName=denisvasilik-ca-root1,id-at-organizationName=denisvasilik,id-at-localityName=Munich,id-at-stateOrProvinceName=Bavaria,id-at-countryName=DE)

This will need some deeper investigation. The mechanism on Linux is probably different. We will probably need to call SSL_get0_peer_CA_list() (or equivalent) to get it.

karelz commented 3 years ago

Triage: Rare scenario, likely won't happen in 6.0. We should take a look later though, add a test and fix it.

wfurt commented 3 years ago

When #45456 is done, we will be able to write tests for this. (without external dependency) We may do the get/set CA list together.

denisvasilik commented 3 years ago

Sounds great to me, I am looking forward working together on this issue.

wfurt commented 3 years ago

The fundamental problem is that the callback runs before the server sends the list. When I run the repro, remoteCertificate is also null and it should not be.

From archeology prospective @bartonjs started with https://github.com/dotnet/corefx/pull/3736 back then in 1.0. Then it was fixed with https://github.com/dotnet/corefx/commit/d3be6bbe9aee8f748f2688fb7bb00c577ac4b11e for HTTP. (and probably not SslStream) and then eventually removed as dead code in https://github.com/dotnet/runtime/pull/43793 when CurlHandler was removed.

To make it work, we will need to bring back CryptoNative_SslCtxSetClientCertCallback. (or something similar) The fact that remoteCertificate is somewhat more concerning.

JulesRenz commented 2 years ago

Hi, I am also currently encountering this issue on Linux and OSX (works fine on windows). However, I am running .NET Core 3.1. (customer requires compliance with .NET Standard 2.1) is there a way to backport this fix onto 3.1 once it is fixed?

Many thanks in advance! JR

karelz commented 2 years ago

All .NET 3.1+ versions are compliant with .NET Standard 2.1. Why exactly does it force you to use .NET Core 3.1 and not something newer - e.g. .NET 6 which is also LTS?

JulesRenz commented 2 years ago

Hi, Thanks for your comment! You are of course right, I was under the illusion, that we would need to stick with 3.1, but now, that you mentioned it, I fail to recall my thought process. Sorry for the inconvenience, I'm looking forward to this fix!

Edit: assuming I'll use .NET 6.0 - will this fix be present there (the current milestone is indicating 7.0.0)