Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add NegotiateClientCertificateAsync support on Windows #51905

Merged
merged 7 commits into from
Jun 2, 2021

Conversation

wfurt
Copy link
Member

@wfurt wfurt commented Apr 27, 2021

This may still need some more work but this change brings basic support for NegotiateClientCertificateAsync e.g. ability to ask for peer certificate late in TLS session.

I wanted to lay out some tests and restrictions as well as provide some implementation so it can be tested with Kestrel.
Linux/OpenSSL will be different with similar structure. We need to trigger the renegotiation and then we simply crank the state machine until it is finished.
On Windows, Tls12 and Tls13 has same code path. Linux will be different as the feature is implemented on top of different mechanisms.

cc: @davidfowl @Tratcher

contributes to #49346

@wfurt wfurt requested a review from a team April 27, 2021 00:52
@wfurt wfurt self-assigned this Apr 27, 2021
@ghost
Copy link

ghost commented Apr 27, 2021

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

Issue Details

This may still need some more work but this change brings basic support for NegotiateClientCertificateAsync e.g. ability to ask for peer certificate late in TLS session.

I wanted to lay out some tests and restrictions as well as provide some implementation so it can be tested with Kestrel.
Linux/OpenSSL will be different with similar structure. We need to trigger the renegotiation and then we simply crank the state machine until it is finished.
On Windows, Tls12 and Tls13 has same code path. Linux will be different as the feature is implemented on top of different mechanisms.

cc: @davidfowl @Tratcher

contributes to #49346

Author: wfurt
Assignees: wfurt
Labels:

area-System.Net.Security

Milestone: -

@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
client.AuthenticateAsClientAsync(clientOptions),
server.AuthenticateAsServerAsync(serverOptions));
// need this to complete TLS 1.3 handshake
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed for 1.3? Not obvious to me...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When AuthenticateAs* is finished with TLS 1.3, the handshake is really not done. (unfortunately)
This is clearly visible on Windows when on first message, the "renegotiation" path is triggered and underlying TLS state will change. So the next line will make sure that we are in fully operational and completed state.

Now, when I try to trigger renegotiation e.g. post-handshake-authentication in this state it fails.
In practice this should not matter as the server will receive request and that will get this completed AFAIK.

I was wondering if I could/should parametrize the test above but I decided not to as the PHA is really different beast as separate test method may be easier to update and understand. For Linux I only have prototype with TLS 1.2 so I don't know if TLS 1.3 will behave same way or not. It is certainly different set of APIs in OpenSSL.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand this...

With TLS 1.3, exactly what TLS state can change after AuthenticateAs*? Is it that after AuthenticateAsServer, we don't necessarily have the client cert yet?

Or are there other cases too?

I'm wondering how a user is supposed to deal with this...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the underlying Schannel. it would essentially trigger the renegotiation path and calling write or another renegotiation would fail at that point. We would have certificate but the Schannel is still probably updating some crypto parameters.

Normally this would not be visible to the user. we had to rework locking quite a bit in 5.0. So I padded many tests that do care about particular behavior with the ping/pong to make sure everything is in steady state.
For TLS 1.2 and bellow we may for example receive SessionTickets. It does not matter too much in this case but it is another example when there is come extra processing after the handshake is done.

I don't know how Linux will behave at this point for TLS 1.3 since it is using different mechanism.
One option may be to remove this test and table TLS 1.3 as separate effort.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think I understand... let me make sure...

The issue here is that with TLS 1.3, the initial handshake isn't actually guaranteed to be done on the server when AuthenticateAsServer completes. (Aside: is this only on the server, or on the client too?)

In practice this is not visible to the user except for this case, right? E.g. if the client sends a cert in the initial handshake, it will be available as soon as AuthenticateAsServer completes, right? Are there any user-visible parameters that might change after AuthenticateAsServer completes?

In this case though, I think the issue is that we can't start the "renegotiation" (or really the TLS 1.3 PHA) until the initial handshake is fully completed, is that correct?

What happens if the user calls NegotiateClientCertAsync in this case, will it fail?

I wonder if we should just wait until the initial handshake completes. Is that possible? Is it hard?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was failing for me if you try renegotiation while in this weird state. I decided (at least for now) that this should not be problem in practice as the server will need receive and consume the request first so all this would actually finish.
The detection may be tricky as it works different way on Linux. The ReplyOnReAuthenticationAsync is never called but I suspect the underlying OpenSSL state is similar as we saw some strange errors before reworking the locking. I can dig through the TLS 1.3 RFC but I think some of the crypto piggybacks on first data to to cut down on roundtrips.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided (at least for now) that this should not be problem in practice as the server will need receive and consume the request first so all this would actually finish.

Yeah, I guess that's reasonable. It seems like the client is always going to send something once the handshake completes, anyway.

Should we add a TLS-1.3 specific test to ensure we fail properly if they call NegotiateClientCertAsync in this weird state?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can dig through the TLS 1.3 RFC but I think some of the crypto piggybacks on first data to to cut down on roundtrips.

I think it would be really good to understand this better. I feel like users could get very confused here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. I can ping the Schannel people as well.

@geoffkizer
Copy link
Contributor

Can we add more negative test cases? Specifically to test:

(1) If there's already-decrypted data in the buffer when Negotiate... is called, it should fail immediately
(2) If there's not, but there is a regular data frame already buffered, Negotiate... should fail immediately
(3) If there's nothing buffered, but the client sends a data frame instead of responding to the negotiate, then we should fail Negotiate... when that read completes

@@ -171,7 +171,7 @@ public async Task SslStream_NetworkStream_Renegotiation_Succeeds(bool useSync)
}
}

[Theory]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, the test failed on Windows7 and I did not have chance to investigate. I'm not sure how important that is but we can possibly throw PNSP if we cannot figure out how to make it work. The flow will need to probably change once we incorporate Linux so I did not want to invest too much into it at the moment.

Main goal of this PR was to provide something useable so you can experiment with Kestrel and provide feedback @Tratcher

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Win7 support isn't critical, so long as we get a good explanation for why it doesn't work. Http.Sys had an issue on Win7 that might be related: https://github.com/dotnet/aspnetcore/blob/8f564897f68944d5f9bf9bded45160fcf3fb0329/src/Servers/HttpSys/src/HttpSysListener.cs#L21-L29

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the link. That is good to know.

@wfurt
Copy link
Member Author

wfurt commented May 13, 2021

Can we add more negative test cases? Specifically to test:

(1) If there's already-decrypted data in the buffer when Negotiate... is called, it should fail immediately
(2) If there's not, but there is a regular data frame already buffered, Negotiate... should fail immediately
(3) If there's nothing buffered, but the client sends a data frame instead of responding to the negotiate, then we should fail Negotiate... when that read completes

I can add more tests. I was somewhat hesitant as some of the restrictions comes from HTTP while SslStream is generally agnostic. But since the main use if for Kestrel, I think it is probably OK to add this for now.
Certainly (1) is OK from TLS state machine prospective.

@wfurt
Copy link
Member Author

wfurt commented May 14, 2021

I added test for the first case. After looking at the other two, I would like to defer it until #50815 goes in as that will change the processing significantly.

The Linux prototype does not use the InternalReadAsync. So when the refactoring goes in I'm inclined to use the new helper functions and process frames directly in RenegotiateAsync. That will also make it easier to check frame type in the production code and write the two remaining tests around TLS frames.

Let me know if that make sense @geoffkizer
If you want to, I can put notes about deferred work to #49346.

@geoffkizer
Copy link
Contributor

I would like to defer it until #50815 goes in as that will change the processing significantly.

Ok, seems reasonable

If you want to, I can put notes about deferred work to #49346.

Yes, let's do that so we don't lose track of this.

@wfurt
Copy link
Member Author

wfurt commented May 20, 2021

notes added. failing MacCatalyst and iOSS are unrelated.

@geoffkizer
Copy link
Contributor

Is this ready for review again? @wfurt is there any unfinished work here?

@wfurt
Copy link
Member Author

wfurt commented May 20, 2021

It should be ready @geoffkizer. Next round of work depends on the ssl stream refactoring and Linux support. #49346 will stay open for a while.

@wfurt
Copy link
Member Author

wfurt commented May 31, 2021

can you please take another look @geoffkizer @stephentoub

@@ -612,6 +665,14 @@ private bool CompleteHandshake(ref ProtocolToken? alertToken, out SslPolicyError
{
_context!.ProcessHandshakeSuccess();

if (_nestedAuth != 1)
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something worth logging

Copy link
Contributor

@geoffkizer geoffkizer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small nit, otherwise looks good.

@wfurt wfurt merged commit e2f9462 into dotnet:main Jun 2, 2021
@wfurt wfurt deleted the renegoWin_49346 branch June 2, 2021 17:10
@ghost ghost locked as resolved and limited conversation to collaborators Jul 2, 2021
@karelz karelz added this to the 6.0.0 milestone Jul 15, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants