diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index 165f8337f..de893e54d 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,3 +1,4 @@  OK - PONG \ No newline at end of file + PONG + True \ No newline at end of file diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index dfdab5f4e..c0021f024 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -390,8 +390,18 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X try { // This only verifies that the chain is valid, but with AllowUnknownCertificateAuthority could trust - // self-signed or partial chained vertificates - var chainIsVerified = chain.Build(certificateToValidate); + // self-signed or partial chained certificates + bool chainIsVerified; + try + { + chainIsVerified = chain.Build(certificateToValidate); + } + catch (ArgumentException ex) when ((ex.ParamName ?? ex.Message) == "certificate" && Runtime.IsMono) + { + // work around Mono cert limitation; report as rejected rather than fault + // (note also the likely .ctor mixup re param-name vs message) + chainIsVerified = false; + } if (chainIsVerified) { // Our method is "TrustIssuer", which means any intermediate cert we're being told to trust diff --git a/src/StackExchange.Redis/Runtime.cs b/src/StackExchange.Redis/Runtime.cs new file mode 100644 index 000000000..879c9c325 --- /dev/null +++ b/src/StackExchange.Redis/Runtime.cs @@ -0,0 +1,9 @@ +using System; +using System.Runtime.InteropServices; + +namespace StackExchange.Redis; + +internal static class Runtime +{ + public static readonly bool IsMono = RuntimeInformation.FrameworkDescription.StartsWith("Mono ", StringComparison.OrdinalIgnoreCase); +} diff --git a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs index 529b29a02..a0d9b5c88 100644 --- a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs +++ b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs @@ -16,30 +16,39 @@ public void CheckIssuerValidity() // Trusting CA explicitly var callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "ca.foo.com.pem")); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None), "subtest 1a"); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 1b"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 1c"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 1d"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 1e"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 1f"); // Trusting the remote endpoint cert directly callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "device01.foo.com.pem")); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None), "subtest 2a"); + if (Runtime.IsMono) + { + // Mono doesn't support this cert usage, reports as rejection (happy for someone to work around this, but isn't high priority) + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 2b"); + } + else + { + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 2b"); + } + + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 2c"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 2d"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 2e"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 2f"); // Attempting to trust another CA (mismatch) callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "ca2.foo.com.pem")); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None), "subtest 3a"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 3b"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 3c"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 3d"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 3e"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 3f"); } private static X509Certificate2 LoadCert(string certificatePath) => new X509Certificate2(File.ReadAllBytes(certificatePath)); diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 451db8c20..0054ce11d 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -68,7 +68,10 @@ public void ParseEndPoint(string data, EndPoint expected, string? expectedFormat [InlineData(CommandFlags.DemandReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, DemandReplica")] // 2-bit flag is hit-and-miss #endif public void CommandFlagsFormatting(CommandFlags value, string expected) - => Assert.Equal(expected, value.ToString()); + { + Assert.SkipWhen(Runtime.IsMono, "Mono has different enum flag behavior"); + Assert.Equal(expected, value.ToString()); + } [Theory] [InlineData(ClientType.Normal, "Normal")]