-
Notifications
You must be signed in to change notification settings - Fork 5.1k
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 TimeSpan UrlRetrievalTimeout { get; set; } | ||
|
||
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))
@@ -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. |
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.
@@ -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; } |
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
if
goes into X509Chain.Build. - DateTime.Now vs DateTime.UtcNow: It was previously
Now
, and Windows needs it (IIRC) in local time, soNow
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.
- 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 TimeSpan UrlRetrievalTimeout { get; set; } | ||
|
||
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?
@@ -109,7 +132,7 @@ public void Reset() | |||
_revocationFlag = X509RevocationFlag.ExcludeRoot; | |||
_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 TimeSpan UrlRetrievalTimeout { get; set; } | ||
|
||
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.
SslAuthenticationOptions
when AuthenticateAs*() is called.VerificationTime
was set, it would resolve toDateTime.Now
e.g. same behavior as now.SslStream
would ignoreX509RevocationMode
andTrust
e.g. complete policy takes precedence. (similar how we prioritizeSslStreamCertificateContext
overX509Certificate
) Then X509ChainPolicy is not provided, we would construct it same way as now from the bits.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: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 theCustomTrustStore
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.