runtime: I'm trying out how the `QuicListener` API to see how suitable it is, but so far I've been unable to even get it past the hello world stage.

I’m trying out how the QuicListener API to see how suitable it is, but so far I’ve been unable to even get it past the hello world stage.

Here is my code:

using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Net.Quic;
using System.Net;
using System.Text;
using System.Net.Security;

var cert = CreateSelfSignedCertificate();

var listener = await QuicListener.ListenAsync(new QuicListenerOptions
{
    ApplicationProtocols = new List<SslApplicationProtocol>
    {
        new SslApplicationProtocol("test")
    },
    ListenEndPoint = IPEndPoint.Parse("127.0.0.1:19999"),
    ConnectionOptionsCallback = (con, hello, token) => ValueTask.FromResult(new QuicServerConnectionOptions
    {
        DefaultStreamErrorCode = 123456,
        DefaultCloseErrorCode = 654321,
        ServerAuthenticationOptions = new SslServerAuthenticationOptions
        {
            ServerCertificate = cert,
            ClientCertificateRequired = false,
            RemoteCertificateValidationCallback = (sender, chain, certificate, errors) => true
        },
    }),
});


_ = Task.Run(async () =>
{
    var con = await listener.AcceptConnectionAsync();
    var stream = await con.AcceptInboundStreamAsync();
    var reader = new StreamReader(stream);
    while (true)
    {
        var data = await reader.ReadLineAsync();
        Console.WriteLine(data);
        await stream.WriteAsync(Encoding.UTF8.GetBytes("World"));
    }
});

try
{

    var value = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions
    {
        RemoteEndPoint = IPEndPoint.Parse("127.0.0.1:19999"),
        DefaultCloseErrorCode = 789,
        DefaultStreamErrorCode = 987,
        ClientAuthenticationOptions = new SslClientAuthenticationOptions
        {
            ClientCertificates = new X509CertificateCollection { cert },
            ApplicationProtocols = new List<SslApplicationProtocol>
        {
            new SslApplicationProtocol("test")
        },
            TargetHost = "localhost",
            RemoteCertificateValidationCallback = (sender, chain, certificate, errors) => true
        }
    });
    var st = await value.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
    await st.WriteAsync(Encoding.UTF8.GetBytes("hello"));
    using var reader = new StreamReader(st);
    Console.WriteLine(await reader.ReadLineAsync());
}
catch (Exception e)
{
    Console.WriteLine(e);
}


X509Certificate2 CreateSelfSignedCertificate()
{
    var ecdsa = ECDsa.Create();
    var certificateRequest = new CertificateRequest("CN=localhost", ecdsa, HashAlgorithmName.SHA256);
    certificateRequest.CertificateExtensions.Add(
        new X509BasicConstraintsExtension(
            certificateAuthority: false,
            hasPathLengthConstraint: false,
            pathLengthConstraint: 0,
            critical: true
        )
    );
    certificateRequest.CertificateExtensions.Add(
        new X509KeyUsageExtension(
            keyUsages:
                X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment |
                X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign,
            critical: false
        )
    );
    certificateRequest.CertificateExtensions.Add(
        new X509EnhancedKeyUsageExtension(
            new OidCollection {
                    new Oid("1.3.6.1.5.5.7.3.2"), // TLS Client auth
                    new Oid("1.3.6.1.5.5.7.3.1")  // TLS Server auth
            },
            false));

    certificateRequest.CertificateExtensions.Add(
        new X509SubjectKeyIdentifierExtension(
            key: certificateRequest.PublicKey,
            critical: false
        )
    );

    var sanBuilder = new SubjectAlternativeNameBuilder();
    sanBuilder.AddDnsName("localhost");
    certificateRequest.CertificateExtensions.Add(sanBuilder.Build());

    return certificateRequest.CreateSelfSigned(DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddYears(5));
}

When I’m trying to run that, I’m getting this error:

System.Security.Authentication.AuthenticationException: Authentication failed. ConfigurationLoadCredential failed: QUIC_STATUS_CERT_NO_CERT
   at System.Net.Quic.ThrowHelper.ThrowIfMsQuicError(Int32 status, String message)
   at System.Net.Quic.MsQuicConfiguration.Create(QuicConnectionOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate certificate, X509Certificate[] intermediates, List`1 alpnProtocols, CipherSuitesPolicy cipherSuitesPolicy, EncryptionPolicy encryptionPolicy)

I’m passing the certificate, obviously, but I believe that this is related to (unstated) requirements in the certificate. I found that this is likely the command to generate an appropriate certificate:

https://github.com/microsoft/msquic/blob/3e89ddcf94210edd2fe21b7c15310922de55142f/src/tools/sample/sample.c#L26

Using that (with):

X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection = certStore.Certificates.Find(
                           X509FindType.FindByThumbprint, "8B9F76CD64DBB6FFBD76C57FCC208677213A427E",
                           false);

var cert = certCollection[0];

Gives a different error:

System.Security.Authentication.AuthenticationException: Authentication failed because the remote party sent a TLS alert: 'UserCanceled'.
   at System.Net.Quic.QuicConnection.HandleEventShutdownInitiatedByTransport(_SHUTDOWN_INITIATED_BY_TRANSPORT_e__Struct& data)
   at System.Net.Quic.QuicConnection.HandleConnectionEvent(QUIC_CONNECTION_EVENT& connectionEvent)
   at System.Net.Quic.QuicConnection.NativeCallback(QUIC_HANDLE* connection, Void* context, QUIC_CONNECTION_EVENT* connectionEvent)

At this point, I’m not sure what is going on and looking at the Quic tests, it looks like I’m doing the right thing.

_Originally posted by @ayende in https://github.com/dotnet/runtime/discussions/82762_

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 22 (22 by maintainers)

Most upvoted comments

I’m wondering if we should throw better exception. Perhaps PNSP would be more appropriate. @nibanks Is there some good way how to tell if MsQuic supports DSA certificates (or whatever) on given platform. The observed behavior is on particularly pleasant.

I did little bit digging and here is what I found. I used you posted code @ayende and the generated certificate fails for SslStream as well - as expected:

System.Security.Authentication.AuthenticationException
  HResult=0x80131501
  Message=Authentication failed because the platform does not support ephemeral keys.
  Source=System.Net.Security
  StackTrace:
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, Boolean newCredentialsRequested) in /_/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs:line 242
   at System.Net.Security.SslStream.AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, Boolean newCredentialsRequested) in /_/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs:line 712

SslStream gives better exception in 8.0 and the export I suggested is unfortunately correct and needed. And then the handshake was failing as you described. After some digging, I found that it fails because of ECDSA but works with RSA. If you change the generation to

- var ecdsa = ECDsa.Create();
+ var rsa = RSA.Create();
+ var certificateRequest = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

then the code prints hello as expected on my Win 11 box so it seems like platform algorithm difference. @nibanks may have some more insight. The 0x80090304 maps to SEC_E_INTERNAL_ERROR and that is certainly strange.

I would suggest to close this there is not much we can do about it at .NET layer. If anything, I would suggest to open issue in msquic repo.

Yes, I have. I’m making extensive use of SslStream and I have tried that exact scenario (and many similar ones) with that with no issue.