blob: 243c0855cff33d0e442c60b2aaa108e7c11fb88b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache.Ignite.Tests;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
/// <summary>
/// SSL tests.
/// </summary>
[SuppressMessage("Security", "CA5359:Do Not Disable Certificate Validation", Justification = "Tests.")]
public class SslTests : IgniteTestsBase
{
private const string CertificatePassword = "123456";
private const string CertificateIssuer =
"E=dev@ignite.apache.org, OU=Apache Ignite CA, O=The Apache Software Foundation, CN=ignite.apache.org";
private static readonly string CertificatePath = Path.Combine(
TestUtils.RepoRootDir, "modules", "runner", "src", "integrationTest", "resources", "ssl", "client.pfx");
private static string SslEndpoint => "localhost:" + (ServerPort + 2);
private static string SslEndpointWithClientAuth => "127.0.0.1:" + (ServerPort + 3);
[Test]
[SuppressMessage("Security", "CA5398:Avoid hardcoded SslProtocols values", Justification = "Tests")]
public async Task TestSslWithoutClientAuthentication()
{
var cfg = new IgniteClientConfiguration
{
Endpoints = { SslEndpoint },
SslStreamFactory = new SslStreamFactory
{
SslClientAuthenticationOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (_, _, _, _) => true
}
}
};
using var client = await IgniteClient.StartAsync(cfg);
var connection = client.GetConnections().Single();
var sslInfo = connection.SslInfo;
Assert.IsNotNull(sslInfo);
Assert.IsFalse(sslInfo!.IsMutuallyAuthenticated);
StringAssert.StartsWith("TLS_", sslInfo.NegotiatedCipherSuiteName);
CollectionAssert.Contains(new[] { SslProtocols.Tls13, SslProtocols.Tls12 }, sslInfo.SslProtocol);
Assert.AreEqual("localhost", sslInfo.TargetHostName);
Assert.IsNull(sslInfo.LocalCertificate);
StringAssert.Contains(CertificateIssuer, sslInfo.RemoteCertificate!.Issuer);
}
[Test]
public async Task TestSslWithClientAuthentication()
{
var cfg = new IgniteClientConfiguration
{
Endpoints = { SslEndpointWithClientAuth },
SslStreamFactory = new SslStreamFactory
{
SslClientAuthenticationOptions = new()
{
RemoteCertificateValidationCallback = (_, _, _, _) => true,
ClientCertificates = new X509Certificate2Collection(new X509Certificate2(CertificatePath, CertificatePassword))
}
},
LoggerFactory = TestUtils.GetConsoleLoggerFactory(LogLevel.Trace)
};
using var client = await IgniteClient.StartAsync(cfg);
var connection = client.GetConnections().Single();
var sslInfo = connection.SslInfo;
Assert.IsNotNull(sslInfo);
Assert.IsTrue(sslInfo!.IsMutuallyAuthenticated);
StringAssert.StartsWith("TLS_", sslInfo.NegotiatedCipherSuiteName);
Assert.AreEqual("127.0.0.1", sslInfo.TargetHostName);
StringAssert.Contains(CertificateIssuer, sslInfo.RemoteCertificate!.Issuer);
StringAssert.Contains(CertificateIssuer, sslInfo.LocalCertificate!.Issuer);
}
[Test]
[SuppressMessage("Security", "CA5398:Avoid hardcoded SslProtocols values", Justification = "Tests.")]
public void TestSslOnClientWithoutSslOnServerThrows()
{
var cfg = GetConfig();
cfg.SslStreamFactory = new SslStreamFactory();
var ex = Assert.ThrowsAsync<AggregateException>(async () => await IgniteClient.StartAsync(cfg));
Assert.IsInstanceOf<IgniteClientConnectionException>(ex?.InnerException);
}
[Test]
public void TestSslOnServerWithoutSslOnClientThrows()
{
var cfg = new IgniteClientConfiguration
{
Endpoints = { SslEndpoint }
};
var ex = Assert.ThrowsAsync<AggregateException>(async () => await IgniteClient.StartAsync(cfg));
Assert.IsInstanceOf<IgniteClientConnectionException>(ex?.InnerException);
}
[Test]
public void TestMissingClientCertThrows()
{
var cfg = new IgniteClientConfiguration
{
Endpoints = { SslEndpointWithClientAuth },
SslStreamFactory = new SslStreamFactory
{
SslClientAuthenticationOptions = new()
{
RemoteCertificateValidationCallback = (_, _, _, _) => true,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
}
}
};
Assert.CatchAsync<Exception>(async () => await IgniteClient.StartAsync(cfg));
}
[Test]
public async Task TestCustomSslStreamFactory()
{
var cfg = new IgniteClientConfiguration
{
Endpoints = { SslEndpoint },
SslStreamFactory = new CustomSslStreamFactory()
};
using var client = await IgniteClient.StartAsync(cfg);
var connection = client.GetConnections().Single();
var sslInfo = connection.SslInfo;
Assert.IsNotNull(sslInfo);
Assert.IsFalse(sslInfo!.IsMutuallyAuthenticated);
}
[Test]
public async Task TestSslStreamFactoryReturnsNullDisablesSsl()
{
var cfg = GetConfig();
cfg.SslStreamFactory = new NullSslStreamFactory();
using var client = await IgniteClient.StartAsync(cfg);
Assert.IsNull(client.GetConnections().First().SslInfo);
}
[Test]
[Platform("Linux", Reason = "CipherSuitesPolicy is not supported on Windows.")]
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Test runs only on Linux")]
public async Task TestCustomCipherSuite()
{
var cipherSuite = OperatingSystem.IsMacOS()
? TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
: TlsCipherSuite.TLS_AES_128_GCM_SHA256;
var cfg = new IgniteClientConfiguration
{
Endpoints = { SslEndpoint },
SslStreamFactory = new SslStreamFactory
{
SslClientAuthenticationOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (_, _, _, _) => true,
CipherSuitesPolicy = new CipherSuitesPolicy([cipherSuite])
}
}
};
using var client = await IgniteClient.StartAsync(cfg);
var connection = client.GetConnections().Single();
var sslInfo = connection.SslInfo;
Assert.IsNotNull(sslInfo);
Assert.IsFalse(sslInfo!.IsMutuallyAuthenticated);
Assert.AreEqual(cipherSuite.ToString(), sslInfo.NegotiatedCipherSuiteName);
}
private class NullSslStreamFactory : ISslStreamFactory
{
public Task<SslStream?> CreateAsync(Stream stream, string targetHost, CancellationToken cancellationToken) =>
Task.FromResult<SslStream?>(null);
}
private class CustomSslStreamFactory : ISslStreamFactory
{
public async Task<SslStream?> CreateAsync(Stream stream, string targetHost, CancellationToken cancellationToken)
{
var sslStream = new SslStream(
innerStream: stream,
leaveInnerStreamOpen: false,
userCertificateValidationCallback: (_, certificate, _, _) => certificate!.Issuer.Contains("ignite"),
userCertificateSelectionCallback: null);
await sslStream.AuthenticateAsClientAsync(targetHost, null, SslProtocols.None, false);
return sslStream;
}
}
}