Skip to content

Commit de503e8

Browse files
committed
Add CloneSslOptionsClonesAllProperties test
1 parent 43d008b commit de503e8

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,6 @@ public static ValueTask<SslServerAuthenticationOptions> OptionsCallback(Connecti
180180
return new ValueTask<SslServerAuthenticationOptions>(options);
181181
}
182182

183-
// TODO: Reflection based test to ensure we clone everything!
184-
// This won't catch issues related to mutable subproperties, but the existing subproperties look like they're mostly immutable.
185-
// The exception are the ApplicationProtocols list which we clone and the ServerCertificate because of methods like Import() and Reset() :(
186183
internal static SslServerAuthenticationOptions CloneSslOptions(SslServerAuthenticationOptions sslOptions) =>
187184
new SslServerAuthenticationOptions
188185
{

src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using System;
55
using System.Collections.Generic;
66
using System.IO.Pipelines;
7+
using System.Linq;
78
using System.Net.Security;
9+
using System.Runtime.InteropServices;
810
using System.Security.Authentication;
911
using System.Security.Cryptography.X509Certificates;
1012
using Microsoft.AspNetCore.Connections;
@@ -429,6 +431,90 @@ public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServer
429431
Assert.Same(selectorCertificate, options.ServerCertificate);
430432
}
431433

434+
[Fact]
435+
public void CloneSslOptionsClonesAllProperties()
436+
{
437+
var propertyNames = typeof(SslServerAuthenticationOptions).GetProperties().Select(property => property.Name).ToList();
438+
439+
CipherSuitesPolicy cipherSuitesPolicy = null;
440+
441+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
442+
{
443+
// The CipherSuitesPolicy ctor throws a PlatformNotSupportedException on Windows.
444+
cipherSuitesPolicy = new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 });
445+
}
446+
447+
// Set options properties to non-default values to verify they're copied.
448+
var options = new SslServerAuthenticationOptions
449+
{
450+
// Defaults to true
451+
AllowRenegotiation = false,
452+
// Defaults to null
453+
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
454+
// Defaults to X509RevocationMode.NoCheck
455+
CertificateRevocationCheckMode = X509RevocationMode.Offline,
456+
// Defaults to null
457+
CipherSuitesPolicy = cipherSuitesPolicy,
458+
// Defaults to false
459+
ClientCertificateRequired = true,
460+
// Defaults to SslProtocols.None
461+
EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls11,
462+
// Defaults to EncryptionPolicy.RequireEncryption
463+
EncryptionPolicy = EncryptionPolicy.NoEncryption,
464+
// Defaults to null
465+
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
466+
// Defaults to null
467+
ServerCertificate = new X509Certificate2(),
468+
// Defaults to null
469+
ServerCertificateContext = SslStreamCertificateContext.Create(_x509Certificate2, additionalCertificates: null, offline: true),
470+
// Defaults to null
471+
ServerCertificateSelectionCallback = (sender, serverName) => null,
472+
};
473+
474+
var clonedOptions = SniOptionsSelector.CloneSslOptions(options);
475+
476+
Assert.NotSame(options, clonedOptions);
477+
478+
Assert.Equal(options.AllowRenegotiation, clonedOptions.AllowRenegotiation);
479+
Assert.True(propertyNames.Remove(nameof(options.AllowRenegotiation)));
480+
481+
// Ensure the List<SslApplicationProtocol> is also cloned since it could be modified by a user callback.
482+
Assert.NotSame(options.ApplicationProtocols, clonedOptions.ApplicationProtocols);
483+
Assert.Equal(Assert.Single(options.ApplicationProtocols), Assert.Single(clonedOptions.ApplicationProtocols));
484+
Assert.True(propertyNames.Remove(nameof(options.ApplicationProtocols)));
485+
486+
Assert.Equal(options.CertificateRevocationCheckMode, clonedOptions.CertificateRevocationCheckMode);
487+
Assert.True(propertyNames.Remove(nameof(options.CertificateRevocationCheckMode)));
488+
489+
Assert.Same(options.CipherSuitesPolicy, clonedOptions.CipherSuitesPolicy);
490+
Assert.True(propertyNames.Remove(nameof(options.CipherSuitesPolicy)));
491+
492+
Assert.Equal(options.ClientCertificateRequired, clonedOptions.ClientCertificateRequired);
493+
Assert.True(propertyNames.Remove(nameof(options.ClientCertificateRequired)));
494+
495+
Assert.Equal(options.EnabledSslProtocols, clonedOptions.EnabledSslProtocols);
496+
Assert.True(propertyNames.Remove(nameof(options.EnabledSslProtocols)));
497+
498+
Assert.Equal(options.EncryptionPolicy, clonedOptions.EncryptionPolicy);
499+
Assert.True(propertyNames.Remove(nameof(options.EncryptionPolicy)));
500+
501+
Assert.Same(options.RemoteCertificateValidationCallback, clonedOptions.RemoteCertificateValidationCallback);
502+
Assert.True(propertyNames.Remove(nameof(options.RemoteCertificateValidationCallback)));
503+
504+
// Technically the ServerCertificate could be reset/reimported, but I'm hoping this is uncommon. Trying to clone the certificate and/or context seems risky.
505+
Assert.Same(options.ServerCertificate, clonedOptions.ServerCertificate);
506+
Assert.True(propertyNames.Remove(nameof(options.ServerCertificate)));
507+
508+
Assert.Same(options.ServerCertificateContext, clonedOptions.ServerCertificateContext);
509+
Assert.True(propertyNames.Remove(nameof(options.ServerCertificateContext)));
510+
511+
Assert.Same(options.ServerCertificateSelectionCallback, clonedOptions.ServerCertificateSelectionCallback);
512+
Assert.True(propertyNames.Remove(nameof(options.ServerCertificateSelectionCallback)));
513+
514+
// Ensure we've checked every property. When new properties get added, we'll have to update this test along with the CloneSslOptions implementation.
515+
Assert.Empty(propertyNames);
516+
}
517+
432518
private class MockCertificateConfigLoader : ICertificateConfigLoader
433519
{
434520
public Dictionary<object, string> CertToPathDictionary { get; } = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);

0 commit comments

Comments
 (0)