-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
Conversation
|
Note regarding the 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. |
|
Tagging subscribers to this area: @dotnet/ncl, @vcsjones Issue DetailsRelated to #59979.
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 The Please comment as you see fit. This is mostly to illustrate the intention and behavior.
|
|
|
||
| public X509ChainPolicy Duplicate() | ||
| { | ||
| var dup = (X509ChainPolicy)this.MemberwiseClone(); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
runtime/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs
Lines 980 to 983 in 29277bc
| if (trust._store != null) | |
| { | |
| chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates); | |
| } |
and this modified application policy:
runtime/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs
Lines 966 to 969 in 29277bc
| 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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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))
| public CipherSuitesPolicy? CipherSuitesPolicy { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Sepcifies specific policy for verifying remote cretrificate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// 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"); ; |
There was a problem hiding this comment.
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))
There was a problem hiding this comment.
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.
| 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; } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about DateTime.UtcNow?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
ifgoes into X509Chain.Build. - DateTime.Now vs DateTime.UtcNow: It was previously
Now, and Windows needs it (IIRC) in local time, soNowprobably 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.
- 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).
|
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 X509ChainPolicy Duplicate() | ||
| { | ||
| var dup = (X509ChainPolicy)this.MemberwiseClone(); |
There was a problem hiding this comment.
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
runtime/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs
Lines 980 to 983 in 29277bc
| if (trust._store != null) | |
| { | |
| chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates); | |
| } |
and this modified application policy:
runtime/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs
Lines 966 to 969 in 29277bc
| 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?
| _verificationFlags = X509VerificationFlags.NoFlag; | ||
| _trustMode = X509ChainTrustMode.System; | ||
| VerificationTime = DateTime.Now; | ||
| _verificationTime = default; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 X509ChainPolicy Duplicate() | ||
| { | ||
| var dup = (X509ChainPolicy)this.MemberwiseClone(); |
There was a problem hiding this comment.
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.
|
Based on the feedback, one option would be to unseal |
No. 😄 |
|
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
Related to #59979.
Allowing users to provide custom policy give option to disable AIA or influence other aspects of validation.
SslAuthenticationOptionswhen AuthenticateAs*() is called.VerificationTimewas set, it would resolve toDateTime.Nowe.g. same behavior as now.SslStreamwould ignoreX509RevocationModeandTruste.g. complete policy takes precedence. (similar how we prioritizeSslStreamCertificateContextoverX509Certificate) Then X509ChainPolicy is not provided, we would construct it same way as now from the bits.ApplicationPolicyis 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:If
ApplicationPolicyis set, we would preserve it, assuming it is what caller really wants.The
Duplicatecreates copy of_extraStorebecause this is place where certificates from the TLS session go e.g. may be unique for each TLS session. I was wondering about theCustomTrustStorebut 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.