|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Security.Authentication;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Microsoft.Quic;
namespace System.Net.Quic;
internal static partial class MsQuicConfiguration
{
private static bool HasPrivateKey(this X509Certificate certificate)
=> certificate is X509Certificate2 certificate2 && certificate2.Handle != IntPtr.Zero && certificate2.HasPrivateKey;
public static MsQuicConfigurationSafeHandle Create(QuicClientConnectionOptions options)
{
SslClientAuthenticationOptions authenticationOptions = options.ClientAuthenticationOptions;
QUIC_CREDENTIAL_FLAGS flags = QUIC_CREDENTIAL_FLAGS.NONE;
flags |= QUIC_CREDENTIAL_FLAGS.CLIENT;
flags |= QUIC_CREDENTIAL_FLAGS.INDICATE_CERTIFICATE_RECEIVED;
flags |= QUIC_CREDENTIAL_FLAGS.NO_CERTIFICATE_VALIDATION;
if (MsQuicApi.UsesSChannelBackend)
{
flags |= QUIC_CREDENTIAL_FLAGS.USE_SUPPLIED_CREDENTIALS;
}
// Find the first certificate with private key, either from selection callback or from a provided collection.
X509Certificate? certificate = null;
ReadOnlyCollection<X509Certificate2>? intermediates = null;
if (authenticationOptions.ClientCertificateContext is not null)
{
certificate = authenticationOptions.ClientCertificateContext.TargetCertificate;
intermediates = authenticationOptions.ClientCertificateContext.IntermediateCertificates;
}
else if (authenticationOptions.LocalCertificateSelectionCallback != null)
{
X509Certificate? selectedCertificate = authenticationOptions.LocalCertificateSelectionCallback(
options,
authenticationOptions.TargetHost ?? string.Empty,
authenticationOptions.ClientCertificates ?? new X509CertificateCollection(),
null,
Array.Empty<string>());
if (selectedCertificate is not null)
{
if (selectedCertificate.HasPrivateKey())
{
certificate = selectedCertificate;
}
else
{
if (NetEventSource.Log.IsEnabled())
{
NetEventSource.Info(options, $"'{certificate}' not selected because it doesn't have a private key.");
}
}
}
}
else if (authenticationOptions.ClientCertificates != null)
{
foreach (X509Certificate clientCertificate in authenticationOptions.ClientCertificates)
{
if (clientCertificate.HasPrivateKey())
{
certificate = clientCertificate;
break;
}
else
{
if (NetEventSource.Log.IsEnabled())
{
NetEventSource.Info(options, $"'{certificate}' not selected because it doesn't have a private key.");
}
}
}
}
return Create(options, flags, certificate, intermediates, authenticationOptions.ApplicationProtocols, authenticationOptions.CipherSuitesPolicy, authenticationOptions.EncryptionPolicy);
}
public static MsQuicConfigurationSafeHandle Create(QuicServerConnectionOptions options, string? targetHost)
{
SslServerAuthenticationOptions authenticationOptions = options.ServerAuthenticationOptions;
QUIC_CREDENTIAL_FLAGS flags = QUIC_CREDENTIAL_FLAGS.NONE;
if (authenticationOptions.ClientCertificateRequired)
{
flags |= QUIC_CREDENTIAL_FLAGS.REQUIRE_CLIENT_AUTHENTICATION;
flags |= QUIC_CREDENTIAL_FLAGS.INDICATE_CERTIFICATE_RECEIVED;
flags |= QUIC_CREDENTIAL_FLAGS.NO_CERTIFICATE_VALIDATION;
}
X509Certificate? certificate = null;
ReadOnlyCollection<X509Certificate2>? intermediates = default;
// the order of checking here matches the order of checking in SslStream
if (authenticationOptions.ServerCertificateSelectionCallback is not null)
{
certificate = authenticationOptions.ServerCertificateSelectionCallback.Invoke(authenticationOptions, targetHost);
}
else if (authenticationOptions.ServerCertificateContext is not null)
{
certificate = authenticationOptions.ServerCertificateContext.TargetCertificate;
intermediates = authenticationOptions.ServerCertificateContext.IntermediateCertificates;
}
else if (authenticationOptions.ServerCertificate is not null)
{
certificate = authenticationOptions.ServerCertificate;
}
if (certificate is null)
{
throw new ArgumentException(SR.Format(SR.net_quic_not_null_ceritifcate, nameof(SslServerAuthenticationOptions.ServerCertificate), nameof(SslServerAuthenticationOptions.ServerCertificateContext), nameof(SslServerAuthenticationOptions.ServerCertificateSelectionCallback)), nameof(options));
}
return Create(options, flags, certificate, intermediates, authenticationOptions.ApplicationProtocols, authenticationOptions.CipherSuitesPolicy, authenticationOptions.EncryptionPolicy);
}
private static MsQuicConfigurationSafeHandle Create(QuicConnectionOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, ReadOnlyCollection<X509Certificate2>? intermediates, List<SslApplicationProtocol>? alpnProtocols, CipherSuitesPolicy? cipherSuitesPolicy, EncryptionPolicy encryptionPolicy)
{
// Validate options and SSL parameters.
if (alpnProtocols is null || alpnProtocols.Count <= 0)
{
throw new ArgumentException(SR.Format(SR.net_quic_not_null_not_empty_connection, nameof(SslApplicationProtocol)), nameof(options));
}
#pragma warning disable SYSLIB0040 // NoEncryption and AllowNoEncryption are obsolete
if (encryptionPolicy == EncryptionPolicy.NoEncryption)
{
throw new PlatformNotSupportedException(SR.Format(SR.net_quic_ssl_option, encryptionPolicy));
}
#pragma warning restore SYSLIB0040
QUIC_SETTINGS settings = default(QUIC_SETTINGS);
settings.IsSet.PeerUnidiStreamCount = 1;
settings.PeerUnidiStreamCount = (ushort)options.MaxInboundUnidirectionalStreams;
settings.IsSet.PeerBidiStreamCount = 1;
settings.PeerBidiStreamCount = (ushort)options.MaxInboundBidirectionalStreams;
if (options.IdleTimeout != TimeSpan.Zero)
{
settings.IsSet.IdleTimeoutMs = 1;
settings.IdleTimeoutMs = options.IdleTimeout != Timeout.InfiniteTimeSpan
? (ulong)options.IdleTimeout.TotalMilliseconds
: 0; // 0 disables the timeout
}
if (options.KeepAliveInterval != TimeSpan.Zero)
{
settings.IsSet.KeepAliveIntervalMs = 1;
settings.KeepAliveIntervalMs = options.KeepAliveInterval != Timeout.InfiniteTimeSpan
? (uint)options.KeepAliveInterval.TotalMilliseconds
: 0; // 0 disables the keepalive
}
settings.IsSet.ConnFlowControlWindow = 1;
settings.ConnFlowControlWindow = (uint)(options._initialReceiveWindowSizes?.Connection ?? QuicDefaults.DefaultConnectionMaxData);
settings.IsSet.StreamRecvWindowBidiLocalDefault = 1;
settings.StreamRecvWindowBidiLocalDefault = (uint)(options._initialReceiveWindowSizes?.LocallyInitiatedBidirectionalStream ?? QuicDefaults.DefaultStreamMaxData);
settings.IsSet.StreamRecvWindowBidiRemoteDefault = 1;
settings.StreamRecvWindowBidiRemoteDefault = (uint)(options._initialReceiveWindowSizes?.RemotelyInitiatedBidirectionalStream ?? QuicDefaults.DefaultStreamMaxData);
settings.IsSet.StreamRecvWindowUnidiDefault = 1;
settings.StreamRecvWindowUnidiDefault = (uint)(options._initialReceiveWindowSizes?.UnidirectionalStream ?? QuicDefaults.DefaultStreamMaxData);
if (options.HandshakeTimeout != TimeSpan.Zero)
{
settings.IsSet.HandshakeIdleTimeoutMs = 1;
settings.HandshakeIdleTimeoutMs = options.HandshakeTimeout != Timeout.InfiniteTimeSpan
? (ulong)options.HandshakeTimeout.TotalMilliseconds
: 0; // 0 disables the timeout
}
QUIC_ALLOWED_CIPHER_SUITE_FLAGS allowedCipherSuites = QUIC_ALLOWED_CIPHER_SUITE_FLAGS.NONE;
if (cipherSuitesPolicy != null)
{
flags |= QUIC_CREDENTIAL_FLAGS.SET_ALLOWED_CIPHER_SUITES;
allowedCipherSuites = CipherSuitePolicyToFlags(cipherSuitesPolicy);
}
if (!MsQuicApi.UsesSChannelBackend)
{
flags |= QUIC_CREDENTIAL_FLAGS.USE_PORTABLE_CERTIFICATES;
}
if (ConfigurationCacheEnabled)
{
return GetCachedCredentialOrCreate(settings, flags, certificate, intermediates, alpnProtocols, allowedCipherSuites);
}
return CreateInternal(settings, flags, certificate, intermediates, alpnProtocols, allowedCipherSuites);
}
private static unsafe MsQuicConfigurationSafeHandle CreateInternal(QUIC_SETTINGS settings, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, ReadOnlyCollection<X509Certificate2>? intermediates, List<SslApplicationProtocol> alpnProtocols, QUIC_ALLOWED_CIPHER_SUITE_FLAGS allowedCipherSuites)
{
if (!MsQuicApi.UsesSChannelBackend && certificate is X509Certificate2 cert && intermediates is null)
{
// MsQuic will not lookup intermediates in local CA store if not explicitly provided,
// so we build the cert context to get on feature parity with SslStream. Note that this code
// path runs after the MsQuicConfigurationCache check.
SslStreamCertificateContext context = SslStreamCertificateContext.Create(cert, additionalCertificates: null, offline: true, trust: null);
intermediates = context.IntermediateCertificates;
}
QUIC_HANDLE* handle;
using MsQuicBuffers msquicBuffers = new MsQuicBuffers();
msquicBuffers.Initialize(alpnProtocols, alpnProtocol => alpnProtocol.Protocol);
ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.ConfigurationOpen(
MsQuicApi.Api.Registration,
msquicBuffers.Buffers,
(uint)msquicBuffers.Count,
&settings,
(uint)sizeof(QUIC_SETTINGS),
(void*)IntPtr.Zero,
&handle),
"ConfigurationOpen failed");
MsQuicConfigurationSafeHandle configurationHandle = new MsQuicConfigurationSafeHandle(handle);
try
{
QUIC_CREDENTIAL_CONFIG config = new QUIC_CREDENTIAL_CONFIG
{
Flags = flags,
AllowedCipherSuites = allowedCipherSuites
};
int status;
if (certificate is null)
{
config.Type = QUIC_CREDENTIAL_TYPE.NONE;
status = MsQuicApi.Api.ConfigurationLoadCredential(configurationHandle, &config);
}
else if (MsQuicApi.UsesSChannelBackend)
{
config.Type = QUIC_CREDENTIAL_TYPE.CERTIFICATE_CONTEXT;
config.CertificateContext = (void*)certificate.Handle;
status = MsQuicApi.Api.ConfigurationLoadCredential(configurationHandle, &config);
}
else
{
config.Type = QUIC_CREDENTIAL_TYPE.CERTIFICATE_PKCS12;
byte[] certificateData;
if (intermediates != null && intermediates.Count > 0)
{
X509Certificate2Collection collection = new X509Certificate2Collection();
collection.Add(certificate);
foreach (X509Certificate2 intermediate in intermediates)
{
collection.Add(intermediate);
}
certificateData = collection.Export(X509ContentType.Pkcs12)!;
}
else
{
certificateData = certificate.Export(X509ContentType.Pkcs12);
}
fixed (byte* ptr = certificateData)
{
QUIC_CERTIFICATE_PKCS12 pkcs12Certificate = new QUIC_CERTIFICATE_PKCS12
{
Asn1Blob = ptr,
Asn1BlobLength = (uint)certificateData.Length,
PrivateKeyPassword = (sbyte*)IntPtr.Zero
};
config.CertificatePkcs12 = &pkcs12Certificate;
status = MsQuicApi.Api.ConfigurationLoadCredential(configurationHandle, &config);
}
}
#if TARGET_WINDOWS
if ((Interop.SECURITY_STATUS)status == Interop.SECURITY_STATUS.AlgorithmMismatch &&
((flags & QUIC_CREDENTIAL_FLAGS.CLIENT) == 0 ? MsQuicApi.Tls13ServerMayBeDisabled : MsQuicApi.Tls13ClientMayBeDisabled))
{
ThrowHelper.ThrowIfMsQuicError(status, SR.net_quic_tls_version_notsupported);
}
if (status == MsQuic.QUIC_STATUS_CERT_NO_CERT && certificate != null && certificate.HasPrivateKey())
{
using Microsoft.Win32.SafeHandles.SafeCertContextHandle safeCertContextHandle = Interop.Crypt32.CertDuplicateCertificateContext(certificate.Handle);
if (safeCertContextHandle.HasEphemeralPrivateKey)
{
throw new AuthenticationException(SR.net_auth_ephemeral);
}
}
#endif
ThrowHelper.ThrowIfMsQuicError(status, "ConfigurationLoadCredential failed");
}
catch
{
configurationHandle.Dispose();
throw;
}
return configurationHandle;
}
private static QUIC_ALLOWED_CIPHER_SUITE_FLAGS CipherSuitePolicyToFlags(CipherSuitesPolicy cipherSuitesPolicy)
{
QUIC_ALLOWED_CIPHER_SUITE_FLAGS flags = QUIC_ALLOWED_CIPHER_SUITE_FLAGS.NONE;
#pragma warning disable CA1416 // not supported on all platforms
foreach (TlsCipherSuite cipher in cipherSuitesPolicy.AllowedCipherSuites)
{
switch (cipher)
{
case TlsCipherSuite.TLS_AES_128_GCM_SHA256:
flags |= QUIC_ALLOWED_CIPHER_SUITE_FLAGS.AES_128_GCM_SHA256;
break;
case TlsCipherSuite.TLS_AES_256_GCM_SHA384:
flags |= QUIC_ALLOWED_CIPHER_SUITE_FLAGS.AES_256_GCM_SHA384;
break;
case TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256:
flags |= QUIC_ALLOWED_CIPHER_SUITE_FLAGS.CHACHA20_POLY1305_SHA256;
break;
case TlsCipherSuite.TLS_AES_128_CCM_SHA256: // not supported by MsQuic (yet?), but QUIC RFC allows it so we ignore it.
default:
// ignore
break;
}
#pragma warning restore
}
if (flags == QUIC_ALLOWED_CIPHER_SUITE_FLAGS.NONE)
{
throw new ArgumentException(SR.net_quic_empty_cipher_suite, nameof(SslClientAuthenticationOptions.CipherSuitesPolicy));
}
return flags;
}
}
|