Skip to content

PoC: support custom X509ChainPolicy in SslStream #69908

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

Closed
wants to merge 1 commit into from

Conversation

wfurt
Copy link
Member

@wfurt wfurt commented May 27, 2022

Related to #59979.
Allowing users to provide custom policy give option to disable AIA or influence other aspects of validation.

  • we would copy provided policy when we capture other properties in SslAuthenticationOptions when AuthenticateAs*() is called.
  • Unless VerificationTime was set, it would resolve to DateTime.Now e.g. same behavior as now.
  • If custom policy is set, SslStream would ignore X509RevocationMode and Trust e.g. complete policy takes precedence. (similar how we prioritize SslStreamCertificateContext over X509Certificate) Then X509ChainPolicy is not provided, we would construct it same way as now from the bits.
  • If ApplicationPolicy is not set, we would always create it and set EKU. This one deviates from above. The may goal is to preserve right behavior for cases when only custom trust is desired, like:
X509ChainPolicy policy = new X509ChainPolicy();

policy.CustomTrustMode = CustomRootTrust;

policy.TrustStore.Add(s_ourPrivateRoot);

// whatever else they want to set.



SslStreamClientOptions clientOpts = ...;

clientOpts.ChainPolicy = policy;



SslStreamServerOptions serverOpts = ...;

serverOpts.ChainPolicy = policy;

If ApplicationPolicy is set, we would preserve it, assuming it is what caller really wants.

The Duplicate creates copy of _extraStore because this is place where certificates from the TLS session go e.g. may be unique for each TLS session. I was wondering about the CustomTrustStore but I felt we don't need extra copy and allocations.

Please comment as you see fit. This is mostly to illustrate the intention and behavior.

@ghost
Copy link

ghost commented May 27, 2022

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.

@ghost
Copy link

ghost commented May 27, 2022

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

Issue Details

Related to #59979.
Allowing users to provide custom policy give option to disable AIA or influence other aspects of validation.

  • we would copy provided policy when we capture other properties in SslAuthenticationOptions when AuthenticateAs*() is called.
  • Unless VerificationTime was set, it would resolve to DateTime.Now e.g. same behavior as now.
  • If custom policy is set, SslStream would ignore X509RevocationMode and Trust e.g. complete policy takes precedence. (similar how we prioritize SslStreamCertificateContext over X509Certificate) Then X509ChainPolicy is not provided, we would construct it same way as now from the bits.
  • If ApplicationPolicy is not set, we would always create it and set EKU. This one deviates from above. The may goal is to preserve right behavior for cases when only custom trust is desired, like:
X509ChainPolicy policy = new X509ChainPolicy();

policy.CustomTrustMode = CustomRootTrust;

policy.TrustStore.Add(s_ourPrivateRoot);

// whatever else they want to set.



SslStreamClientOptions clientOpts = ...;

clientOpts.ChainPolicy = policy;



SslStreamServerOptions serverOpts = ...;

serverOpts.ChainPolicy = policy;

If ApplicationPolicy is set, we would preserve it, assuming it is what caller really wants.

The Duplicate creates copy of _extraStore because this is place where certificates from the TLS session go e.g. may be unique for each TLS session. I was wondering about the CustomTrustStore but I felt we don't need extra copy and allocations.

Please comment as you see fit. This is mostly to illustrate the intention and behavior.

Author: wfurt
Assignees: wfurt
Labels:

area-System.Net.Security

Milestone: -


public TimeSpan UrlRetrievalTimeout { get; set; }

public X509ChainPolicy Duplicate()
{
var dup = (X509ChainPolicy)this.MemberwiseClone();
Copy link
Member

Choose a reason for hiding this comment

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

Everything should just be cloned manually. All of the collection values need to be cloned to avoid delayed or concurrent modification (e.g. you're potentially adding to ApplicationPolicy, which could be an empty, non-null collection).

Copy link
Member Author

Choose a reason for hiding this comment

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

could be done. I did not want to duplicate things we don't really need to save allocations.

Copy link
Member

Choose a reason for hiding this comment

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

If it's going to be a public API and Duplicate doesn't duplicate everything, I think that would be somewhat odd. I think most callers would expect a deep copy, and not a shallow copy.

Things that would need to be copied:

internal OidCollection? _applicationPolicy;
internal OidCollection? _certificatePolicy;
internal X509Certificate2Collection? _extraStore;
internal X509Certificate2Collection? _customTrustStore;

We know we need to copy _extraStore. So the number of allocations we would save are three class objects. Since X509Certificate2 and Oid are (mostly) immutable, I don't think the individual objects themselves need to be copied, just the collections.

Three allocations for a "behaves as expected" API seems acceptable to me, and the copies would only need to be done if not null.


Though I wonder if we should be deep copying everything anyway. This location is modifying _customTrustStore

if (trust._store != null)
{
chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates);
}

and this modified application policy:

if (chain.ChainPolicy.ApplicationPolicy.Count == 0)
{
// Authenticate the remote party: (e.g. when operating in server mode, authenticate the client).
chain.ChainPolicy.ApplicationPolicy.Add(_sslAuthenticationOptions.IsServer ? s_clientAuthOid : s_serverAuthOid);

Since those are not copied by Duplicate, would those modifications be observed by callers?

Copy link
Member

Choose a reason for hiding this comment

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

The alternative, of course, is not ship this as a public API if we are sensitive to the object allocations.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking about it. I had offline chat with @bartonjs and this PR is out to flesh out possible behavior to API proposed in #59979 (comment) . The policy is not in center but we should make sure we can achieve reasonable behavior. (and solve the underlying use cases (listed only one here))

@@ -74,5 +74,10 @@ public EncryptionPolicy EncryptionPolicy
/// Use extreme caution when changing this setting.
/// </summary>
public CipherSuitesPolicy? CipherSuitesPolicy { get; set; }

/// <summary>
/// Sepcifies specific policy for verifying remote cretrificate.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Sepcifies specific policy for verifying remote cretrificate.
/// Specifies the policy for verifying the remote certificate.

if (CertificateContext.Trust != null && sslServerAuthenticationOptions.ChainPolicy != null &&
sslServerAuthenticationOptions.ChainPolicy.TrustMode != X509ChainTrustMode.CustomRootTrust)
{
throw new InvalidOperationException("Unable to use custom trust without custom root"); ;
Copy link
Member

Choose a reason for hiding this comment

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

I think the intent here is that if you specified a trust value and a policy that you have to be in custom root trust on the policy... but it doesn't verify that they're the same contents.

Unless it's going to verify they're the same set, I'd just ignore it. Trust would still control the trust list in the handshake (if opted in), but if there's also a policy then it'll do what the policy says. (That also solves the problem of what to do about future potential trust modes, like CustomAnchors (stop at intermediates that are in the collection, don't have to be roots) or SystemOrCustomRootTrust (which isn't very likely))

Copy link
Member Author

Choose a reason for hiding this comment

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

That would be ok. We can define what the behavior is.

@@ -2908,6 +2908,7 @@ public sealed partial class X509ChainPolicy
public System.TimeSpan UrlRetrievalTimeout { get { throw null; } set { } }
public System.Security.Cryptography.X509Certificates.X509VerificationFlags VerificationFlags { get { throw null; } set { } }
public System.DateTime VerificationTime { get { throw null; } set { } }
public System.Security.Cryptography.X509Certificates.X509ChainPolicy Duplicate() { throw null; }
Copy link
Member

@rzikm rzikm May 30, 2022

Choose a reason for hiding this comment

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

Why not name it Clone? is this some specific convention?

Copy link
Member Author

Choose a reason for hiding this comment

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

Clone would be fine. I don't care that much about the name. The big question is is if we want to add public method here (like I did) or if we hide it and make it visible only to runtime.

Copy link
Member

Choose a reason for hiding this comment

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

I thought that since we have MemberwiseClone then it is more common to use Clone instead of Duplicate or other names, but I don't have too strong opinion on this.

{
get
{
return _verificationTime.Equals(default(DateTime)) ? DateTime.Now : _verificationTime;
Copy link
Member

Choose a reason for hiding this comment

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

what about DateTime.UtcNow?

Copy link
Member Author

Choose a reason for hiding this comment

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

I used DateTime.Now because it is used by the Reset. I can change that if we want to.

Copy link
Member

Choose a reason for hiding this comment

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

UtcNow is more efficient AFAIK, so unless it would change behavior I think changing it would be good idea.

Copy link
Member

Choose a reason for hiding this comment

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

I would consider using a nullable DateTime? instead of DateTime.MinValue as a null-sentinel.

Copy link
Member Author

Choose a reason for hiding this comment

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

That would be great but DateTime is struct. I originally thought about adding internal 'bool' to track if the time was set or not. The use case here is single policy that is used over time for each ssl session. So unless set explicitly, we would like to default to what ever now it is when new session is created.

Copy link
Member

Choose a reason for hiding this comment

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

There are a couple of things here, and I glossed over them.

  • There's a question of should the default be "when Build is called" or "when the Policy object was built". We've seen from people needing to work around it that people who save policy objects long term thought the default was "when Build is called".
  • Using a "have you called the property setter" secret internal state makes things a bit weird, since there's no way to return to that state after the fact.
  • We can't change the property to be a nullable
  • There's a question of should the getter be honest and say it's a sentinel value, and the if goes into X509Chain.Build.
  • DateTime.Now vs DateTime.UtcNow: It was previously Now, and Windows needs it (IIRC) in local time, so Now probably makes more sense... but I didn't comment about previously this because this is a proof of concept, not something ready to merge.
  • Duplicate vs Clone: I don't have strong opinions. Pick a name, and we'll let API Review decide which one is best.
    • I don't have a problem with the public concept of cloning the object, but I think it should be a semi-shallow clone: The collections are unique, the objects in them are not. (So we don't clone any X509Certificate2 instances or Oid instances, they're both mostly-read-only anyways).
      • The clone can certainly turn empty collections back into null ones.

@wfurt
Copy link
Member Author

wfurt commented May 31, 2022

Let me know @bartonjs if there are conceptual issues. We can sort out details during reviews but I would lie to write API proposal ready for API review.


public TimeSpan UrlRetrievalTimeout { get; set; }

public X509ChainPolicy Duplicate()
{
var dup = (X509ChainPolicy)this.MemberwiseClone();
Copy link
Member

Choose a reason for hiding this comment

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

If it's going to be a public API and Duplicate doesn't duplicate everything, I think that would be somewhat odd. I think most callers would expect a deep copy, and not a shallow copy.

Things that would need to be copied:

internal OidCollection? _applicationPolicy;
internal OidCollection? _certificatePolicy;
internal X509Certificate2Collection? _extraStore;
internal X509Certificate2Collection? _customTrustStore;

We know we need to copy _extraStore. So the number of allocations we would save are three class objects. Since X509Certificate2 and Oid are (mostly) immutable, I don't think the individual objects themselves need to be copied, just the collections.

Three allocations for a "behaves as expected" API seems acceptable to me, and the copies would only need to be done if not null.


Though I wonder if we should be deep copying everything anyway. This location is modifying _customTrustStore

if (trust._store != null)
{
chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates);
}

and this modified application policy:

if (chain.ChainPolicy.ApplicationPolicy.Count == 0)
{
// Authenticate the remote party: (e.g. when operating in server mode, authenticate the client).
chain.ChainPolicy.ApplicationPolicy.Add(_sslAuthenticationOptions.IsServer ? s_clientAuthOid : s_serverAuthOid);

Since those are not copied by Duplicate, would those modifications be observed by callers?

@@ -109,7 +132,7 @@ public void Reset()
_revocationFlag = X509RevocationFlag.ExcludeRoot;
_verificationFlags = X509VerificationFlags.NoFlag;
_trustMode = X509ChainTrustMode.System;
VerificationTime = DateTime.Now;
_verificationTime = default;
Copy link
Member

Choose a reason for hiding this comment

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

This would be a breaking change, correct? Previous behavior: _verificationTime would be whatever DateTime.Now is when Reset or .ctor was created, while now VerificationTime is "now" every time it is accessed until it has been explicitly set.

I don't think this is a bad change, I just would want to understand why, and, consider what might break of consumers.

{
get
{
return _verificationTime.Equals(default(DateTime)) ? DateTime.Now : _verificationTime;
Copy link
Member

Choose a reason for hiding this comment

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

I would consider using a nullable DateTime? instead of DateTime.MinValue as a null-sentinel.


public TimeSpan UrlRetrievalTimeout { get; set; }

public X509ChainPolicy Duplicate()
{
var dup = (X509ChainPolicy)this.MemberwiseClone();
Copy link
Member

Choose a reason for hiding this comment

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

The alternative, of course, is not ship this as a public API if we are sensitive to the object allocations.

@wfurt
Copy link
Member Author

wfurt commented May 31, 2022

Based on the feedback, one option would be to unseal X509ChainPolicy Then the SslStream (and QUIC) should do whatever need to be done to efficiently process n ew sessions. if we end up with public clone/duplicate method I agree it would be strange to make it optimize for just TLS processing.

@bartonjs
Copy link
Member

bartonjs commented May 31, 2022

one option would be to unseal X509ChainPolicy

No. 😄

@wfurt wfurt marked this pull request as draft June 7, 2022 16:20
@ghost ghost closed this Jul 7, 2022
@ghost
Copy link

ghost commented Jul 7, 2022

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@karelz karelz added this to the 7.0.0 milestone Jul 19, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 18, 2022
This pull request was closed.
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