Skip to content

Proposed SslStream additions for ALPN  #23157

@Drawaes

Description

@Drawaes

Latest Proposal

https://github.com/dotnet/corefx/issues/23177#issuecomment-332995338

namespace System.Net.Security
{
public partial class SslStream
{
    public Task AuthenticateAsServerAsync(SslServerAuthenticationOptions options, CancellationToken cancellation) { }
    public Task AuthenticateAsClientAsync(SslClientAuthenticationOptions options, CancellationToken cancellation) { }
    
    public SslApplicationProtocol NegotiatedApplicationProtocol { get; }
}

public class SslServerAuthenticationOptions
{
    public bool AllowRenegotiation { get; set; }
    public X509Certificate ServerCertificate { get; set; }
    public bool ClientCertificateRequired { get; set; }
    public SslProtocols EnabledSslProtocols { get; set; }
    public X509RevocationMode CheckCertificateRevocation { get; set; }
    public IList<SslApplicationProtocol> ApplicationProtocols { get; set; }
    public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
    public EncryptionPolicy EncryptionPolicy { get; set; }
}

public class SslClientAuthenticationOptions
{
    public bool AllowRenegotiation { get; set; }
    public string TargetHost { get; set; }
    public X509CertificateCollection ClientCertificates { get; set; }
    public LocalCertificateSelectionCallback LocalCertificateSelectionCallback { get; set; }
    public SslProtocols EnabledSslProtocols { get; set; }
    public X509RevocationMode CheckCertificateRevocation { get; set; }
    public IList<SslApplicationProtocol> ApplicationProtocols { get; set; }
    public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
    public EncryptionPolicy EncryptionPolicy { get; set; }
}

public struct SslApplicationProtocol : IEquatable<SslApplicationProtocol>
{
    public static readonly SslApplicationProtocol Http2;
    public static readonly SslApplicationProtocol Http11;
    // Adding other IANA values, is left based on customer input.

    public SslApplicationProtocol(byte[] protocol) { }

    public SslApplicationProtocol(string protocol) { } 
  
    public ReadOnlyMemory<byte> Protocol { get; }

    public bool Equals(SslApplicationProtocol other) { }
    public override bool Equals(object obj) { }
    public override int GetHashCode() { }
    public override string ToString() { } 
    public static bool operator ==(SslApplicationProtocol left, SslApplicationProtocol right) { }
    public static bool operator !=(SslApplicationProtocol left, SslApplicationProtocol right) { }
}
}

Previous Proposal

Change log:

  • Updated = Removed results object to keep new objects down
  • Updated = Meeting notes
  • Updated = Removed string overload as there is no clear way in code to tell the user that it's utf-8 and only adds a potential trap
  • Updated = Put that string overload back under protest, but decision was made in a review meeting

References #23107

Rationale

Server Name Indication and Application Layer Protocol Negotiation are two TLS extensions that are missing currently from SslStream. They are needed urgently to support Http/2 (ALPN) and the ability to run Kestrel as an edge server (SNI). There are also many other customer applications that require this, including Clients being able to use HTTP/2 (Mananed HttpClient has an Http/2 support open issue).

These have been outstanding for a long period of time, for a number of reasons. One major reason is that any change will cause an increase in overloading of the Authenticate methods which have become unwieldy and are brittle when adding new options.

There will be more options coming with other TLS extensions available now (max fragment size for restricted memory clients for instance) and having a mechanism to add these without major API changes seems like a sensible change.

Proposed API Change

Originally I suggested overloading the Authenticate methods, however discussions in #23107 have changed my mind. Thanks to @Tratcher for this concept

public partial class SslStream
{
   public Task AuthenticateAsServerAsync(SslServerAuthenticationOptions options, CancellationToken cancellation) { }
   public Task AuthenticateAsClientAsync(SslClientAuthenticationOptions options, CancellationToken cancellation) { }
    
   public SslApplicationProtocol NegotiatedApplicationProtocol {get;}
}

public class SslServerAuthenticationOptions
{
   public bool AllowRenegotiation { get; set; }
   public X509Certificate ServerCertificate { get; set; }
   public bool ClientCertificateRequired { get; set; }
   public SslProtocols EnabledSslProtocols { get; set; }
   public X509RevocationMode CheckCertificateRevocation { get; set; }
   public IList<SslApplicationProtocol> ApplicationProtocols { get; set; }
   public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
   public EncryptionPolicy EncryptionPolicy { get; set; }
}

public class SslClientAuthenticationOptions
{
   public bool AllowRenegotiation { get; set; }
   public string TargetHost { get; set; }
   public X509CertificateCollection ClientCertificates { get; set; }
   public LocalCertificateSelectionCallback LocalCertificateSelectionCallback { get; set; }
   public SslProtocols EnabledSslProtocols { get; set; }
   public X509RevocationMode CheckCertificateRevocation { get; set; }
   public IList<SslApplicationProtocol> ApplicationProtocols { get; set; }
   public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
   public EncryptionPolicy EncryptionPolicy { get; set; }
}

public struct SslApplicationProtocol : IEquatable<SslApplicationProtocol>
{
    public static readonly SslApplicationProtocol Http2;
    public static readonly SslApplicationProtocol Http11;
    // Adding other IANA values, is left based on customer input.

    public SslApplicationProtocol(byte[] protocol) { }

   public SslApplicationProtocol(string protocol) { } // assumes utf-8 and public override 
  
    public ReadOnlyMemory<byte> Protocol { get; }

    public bool Equals(SslApplicationProtocol other) { }
    public override bool Equals(object obj) { }
    public override int GetHashCode() { }
    public override string ToString() { } // For debugger. utf-8 or a string representation of the raw bytes. E.g. "0x68 0x74 0x74 0x70 0x2f 0x31 0x2e 0x31"
    public static bool operator ==(SslApplicationProtocol left, SslApplicationProtocol right) { }
    public static bool operator !=(SslApplicationProtocol left, SslApplicationProtocol right) { }
}

Meeting Notes 22-Sep-2017

  1. Add ToString() to SslApplicationProtocol to get string version of the bytes.
  2. Add string ctor to SslApplicationProtocol for usability, this will assume it's utf8 string.
  3. ReadOnlyMemory is not immutable, we need to copy the bytes in the ctor, so taking a byte[] instead of ReadOnlyMemory

Meeting Notes 5-Sep-2017

  1. Use the updated API proposal above with options bags on the Authenticate methods.
  2. Introduce an ApplicationProtocol type so the ALPN values can be correctly defined as raw bytes, have shared constants, and have equality operators.
  3. Disallow mixing calls between the old constructors and the new methods. Only the minimal constructors taking the inner stream and ownership bool will be supported.
  4. There is still some pending discussion on the factory approach.

Further Notes

  1. This will mean future options can be added without causing binary compatibility issues (increasing properties on concrete classes rather than changing overloads)
  2. Non async methods shouldn't be supported for the new methods as it is an async operation under the hood so hiding the threads goes against current framework thinking. Consumers can wrap the async methods with blocking if they so wish (see modern HttpClient )
  3. I snuck max fragment in for the client, but I am happy to have this dropped if it is a sticking point
  4. Cancellation tokens are there for both methods as per current framework thinking
  5. ValueTask wasn't considered because there will be very few times this doesn't cause async operations, but if the new standard is to use ValueTask that is fine as well
  6. A static list of the Http 1.1, Http/2 and Http/2 over TLS should be provided to stop users having to look it up.
  7. Helper methods on the auth results should be provided so users don't have to look up what the string representations are in this case I added one for IsHttp2.

Potential Implementation Issues

There are now a number of parameters that could be modified during an connection being created. One of these is the certificate dictionary. If the consumer changes these by accidentally reusing the config options for a differently configured connection you could run into a security issue. One option is to have a builder but this is frowned upon as an API pattern and instead mostly used in higher level libraries/frameworks (ASP.NET for instance). This is solvable via taking an internal snapshot of the options but will need to be considered in any threat modelling.

Example Usage

var options = new ServerAuthenticationOptions()
{
    EnabledProtocols = SslProtocols.Tls12 | SslPRotocols.Tls11,
    ApplicationProtocols = new List<ApplicationProtocol>()
    {
         ApplicationProtocol.Http2,
         ApplicationProtocol.Http11
    }
};
var secureStream = new SslStream(innerStream);
await secureStream(options, token);

References

#17677 SNI
#15813 ALPN
#23107 Previous API proposal
RFC 7301 ALPN
RFC 3546 SNI and Max fragment
IANA ALPN Protocol Ids

[EDIT] Updated formatting by @karelz

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions