Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Xunit;

namespace System.Net.Security.Tests
{
public partial class SslAuthenticationOptionsTests
{
// This test exercises SslStreamCertificateContext.Create with intermediate certificates, which
// triggers a generic Dictionary instantiation whose IL body is stripped from the Apple-mobile
// CoreCLR ReadyToRun composite image, crashing the test app at startup. Compile it on desktop
// platforms only.
[Fact]
public void UpdateOptions_ServerCertificateContextProvided_DoesNotDisposeCallerContext()
{
// Build a certificate chain: root → intermediate → leaf.
// Use ECDSA P-256 keys: fast and accepted on FIPS-mode platforms.
DateTimeOffset now = DateTimeOffset.UtcNow;

using ECDsa rootKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var rootReq = new CertificateRequest("CN=TestRoot", rootKey, HashAlgorithmName.SHA256);
rootReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
using X509Certificate2 rootCert = rootReq.CreateSelfSigned(now.AddDays(-1), now.AddDays(365));

using ECDsa intermediateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var intermediateReq = new CertificateRequest("CN=TestIntermediate", intermediateKey, HashAlgorithmName.SHA256);
intermediateReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
using X509Certificate2 intermediatePub = intermediateReq.Create(rootCert, now.AddDays(-1), now.AddDays(365), new byte[] { 1 });
using X509Certificate2 intermediateWithKey = intermediatePub.CopyWithPrivateKey(intermediateKey);

using ECDsa leafKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var leafReq = new CertificateRequest("CN=TestLeaf", leafKey, HashAlgorithmName.SHA256);
using X509Certificate2 leafPub = leafReq.Create(intermediateWithKey, now.AddDays(-1), now.AddDays(365), new byte[] { 2 });
using X509Certificate2 leafWithKey = leafPub.CopyWithPrivateKey(leafKey);

// Create a caller-owned context with an intermediate certificate
SslStreamCertificateContext callerContext = SslStreamCertificateContext.Create(
leafWithKey,
new X509Certificate2Collection { intermediateWithKey },
offline: true);

// Simulate first UpdateOptions call: bare ServerCertificate creates and owns a context
var options = new SslAuthenticationOptions();
SslStreamCertificateContext ownedContext = SslStreamCertificateContext.Create(
leafWithKey,
new X509Certificate2Collection { intermediateWithKey },
offline: true);
// Capture the internal intermediate cert object before it is released
Assert.NotEmpty(ownedContext.IntermediateCertificates);
X509Certificate2 ownedIntermediate = ownedContext.IntermediateCertificates[0];
options.CertificateContext = ownedContext;
options.OwnsCertificateContext = true;

// Simulate second UpdateOptions call: caller provides a ServerCertificateContext.
// Before the fix, OwnsCertificateContext was not reset to false here, causing Dispose()
// to incorrectly call ReleaseResources() on the caller-owned context.
options.UpdateOptions(new SslServerAuthenticationOptions
{
ServerCertificateContext = callerContext,
});

Assert.False(options.OwnsCertificateContext);
Assert.Same(callerContext, options.CertificateContext);

// UpdateOptions should have released the previously-owned context's resources.
// After X509Certificate2.Dispose(), Handle returns IntPtr.Zero.
Assert.NotNull(ownedIntermediate);
Assert.Equal(IntPtr.Zero, ownedIntermediate.Handle);

// Dispose should NOT release the caller's context
options.Dispose();

// Verify that the caller's intermediate certificates were not disposed
Assert.Equal(1, callerContext.IntermediateCertificates.Count);
// Export() requires the native certificate handle; a CryptographicException would indicate the certificate was incorrectly disposed.
foreach (X509Certificate2 cert in callerContext.IntermediateCertificates)
{
Assert.Null(Record.Exception(() => cert.Export(X509ContentType.Cert)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace System.Net.Security.Tests
{
public class SslAuthenticationOptionsTests
public partial class SslAuthenticationOptionsTests
{
private readonly SslClientAuthenticationOptions _clientOptions = new SslClientAuthenticationOptions();
private readonly SslServerAuthenticationOptions _serverOptions = new SslServerAuthenticationOptions();
Expand Down Expand Up @@ -164,74 +164,5 @@ public void EncryptionPolicy_Get_Set_Succeeds()
Assert.Throws<ArgumentException>(() => _clientOptions.EncryptionPolicy = (EncryptionPolicy)3);
Assert.Throws<ArgumentException>(() => _serverOptions.EncryptionPolicy = (EncryptionPolicy)3);
}

[Fact]
public void UpdateOptions_ServerCertificateContextProvided_DoesNotDisposeCallerContext()
{
// Build a certificate chain: root → intermediate → leaf.
// Use ECDSA P-256 keys: fast and accepted on FIPS-mode platforms.
DateTimeOffset now = DateTimeOffset.UtcNow;

using ECDsa rootKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var rootReq = new CertificateRequest("CN=TestRoot", rootKey, HashAlgorithmName.SHA256);
rootReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
using X509Certificate2 rootCert = rootReq.CreateSelfSigned(now.AddDays(-1), now.AddDays(365));

using ECDsa intermediateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var intermediateReq = new CertificateRequest("CN=TestIntermediate", intermediateKey, HashAlgorithmName.SHA256);
intermediateReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
using X509Certificate2 intermediatePub = intermediateReq.Create(rootCert, now.AddDays(-1), now.AddDays(365), new byte[] { 1 });
using X509Certificate2 intermediateWithKey = intermediatePub.CopyWithPrivateKey(intermediateKey);

using ECDsa leafKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var leafReq = new CertificateRequest("CN=TestLeaf", leafKey, HashAlgorithmName.SHA256);
using X509Certificate2 leafPub = leafReq.Create(intermediateWithKey, now.AddDays(-1), now.AddDays(365), new byte[] { 2 });
using X509Certificate2 leafWithKey = leafPub.CopyWithPrivateKey(leafKey);

// Create a caller-owned context with an intermediate certificate
SslStreamCertificateContext callerContext = SslStreamCertificateContext.Create(
leafWithKey,
new X509Certificate2Collection { intermediateWithKey },
offline: true);

// Simulate first UpdateOptions call: bare ServerCertificate creates and owns a context
var options = new SslAuthenticationOptions();
SslStreamCertificateContext ownedContext = SslStreamCertificateContext.Create(
leafWithKey,
new X509Certificate2Collection { intermediateWithKey },
offline: true);
// Capture the internal intermediate cert object before it is released
Assert.NotEmpty(ownedContext.IntermediateCertificates);
X509Certificate2 ownedIntermediate = ownedContext.IntermediateCertificates[0];
options.CertificateContext = ownedContext;
options.OwnsCertificateContext = true;

// Simulate second UpdateOptions call: caller provides a ServerCertificateContext.
// Before the fix, OwnsCertificateContext was not reset to false here, causing Dispose()
// to incorrectly call ReleaseResources() on the caller-owned context.
options.UpdateOptions(new SslServerAuthenticationOptions
{
ServerCertificateContext = callerContext,
});

Assert.False(options.OwnsCertificateContext);
Assert.Same(callerContext, options.CertificateContext);

// UpdateOptions should have released the previously-owned context's resources.
// After X509Certificate2.Dispose(), Handle returns IntPtr.Zero.
Assert.NotNull(ownedIntermediate);
Assert.Equal(IntPtr.Zero, ownedIntermediate.Handle);

// Dispose should NOT release the caller's context
options.Dispose();

// Verify that the caller's intermediate certificates were not disposed
Assert.Equal(1, callerContext.IntermediateCertificates.Count);
// Export() requires the native certificate handle; a CryptographicException would indicate the certificate was incorrectly disposed.
foreach (X509Certificate2 cert in callerContext.IntermediateCertificates)
{
Assert.Null(Record.Exception(() => cert.Export(X509ContentType.Cert)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@
Link="Common\System\Net\Security\SslKeyLogger.cs" />
</ItemGroup>

<!-- Compiled on desktop only: crashes Apple-mobile CoreCLR at startup due to R2R IL-body stripping. -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows' or '$(TargetPlatformIdentifier)' == 'unix' or '$(TargetPlatformIdentifier)' == 'osx'">
<Compile Include="SslAuthenticationOptionsTests.Desktop.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'ios'">
<Compile Include="..\..\src\System\Net\Security\SslStreamCertificateContext.OSX.cs" />
<!-- reuse Unix SafeDeleteSslContext since it has no platform specific code -->
Expand Down