diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index a3e3b094..8ad62687 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -18,6 +18,11 @@ jobs: build_and_Test: name: Build and test runs-on: ubuntu-latest + environment: REDIS_USER + env: + USER_NAME: ${{ secrets.USER_NAME }} + PASSWORD: ${{ secrets.PASSWORD }} + ENDPOINT: ${{ secrets.ENDPOINT }} steps: - uses: actions/checkout@v3 - name: .NET Core 6 @@ -35,9 +40,19 @@ jobs: - name: Build run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true - name: Test - run: dotnet test -f net6.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + run: | + echo "${{secrets.REDIS_CA_PEM}}" > tests/NRedisStack.Tests/bin/Debug/net6.0/redis_ca.pem + echo "${{secrets.REDIS_USER_CRT}}" > tests/NRedisStack.Tests/bin/Debug/net6.0/redis_user.crt + echo "${{secrets.REDIS_USER_PRIVATE_KEY}}" > tests/NRedisStack.Tests/bin/Debug/net6.0/redis_user_private.key + ls -R + dotnet test -f net6.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - name: Test - run: dotnet test -f net7.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + run: | + echo "${{secrets.REDIS_CA_PEM}}" > tests/NRedisStack.Tests/bin/Debug/net7.0/redis_ca.pem + echo "${{secrets.REDIS_USER_CRT}}" > tests/NRedisStack.Tests/bin/Debug/net7.0/redis_user.crt + echo "${{secrets.REDIS_USER_PRIVATE_KEY}}" > tests/NRedisStack.Tests/bin/Debug/net7.0/redis_user_private.key + ls -R + dotnet test -f net7.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - name: Codecov uses: codecov/codecov-action@v3 with: @@ -48,6 +63,11 @@ jobs: build_and_test_windows: name: Build and Test on Windows runs-on: windows-latest + environment: REDIS_USER + env: + USER_NAME: ${{ secrets.USER_NAME }} + PASSWORD: ${{ secrets.PASSWORD }} + ENDPOINT: ${{ secrets.ENDPOINT }} steps: - uses: actions/checkout@v3 - uses: Vampire/setup-wsl@v2 @@ -59,11 +79,18 @@ jobs: sudo apt-get update sudo apt-get install curl -y && sudo apt-get install gpg -y && apt-get install lsb-release -y && apt-get install libgomp1 -y curl https://packages.redis.io/redis-stack/redis-stack-server-${{env.redis_stack_version}}.jammy.x86_64.tar.gz -o redis-stack.tar.gz - tar xf redis-stack.tar.gz + tar xf redis-stack.tar.gz - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true + - name: Save test certificates + shell: wsl-bash {0} + run: | + echo "${{secrets.REDIS_CA_PEM}}" > tests/NRedisStack.Tests/bin/Debug/net481/redis_ca.pem + echo "${{secrets.REDIS_USER_CRT}}" > tests/NRedisStack.Tests/bin/Debug/net481/redis_user.crt + echo "${{secrets.REDIS_USER_PRIVATE_KEY}}" > tests/NRedisStack.Tests/bin/Debug/net481/redis_user_private.key + ls -R - name: Test shell: cmd run: | diff --git a/.gitignore b/.gitignore index 25913044..3cf3c6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -399,4 +399,9 @@ FodyWeavers.xsd .idea tests/NRedisStack.Tests/lcov.net7.0.info tests/NRedisStack.Tests/lcov.net6.0.info -tests/NRedisStack.Tests/lcov.info \ No newline at end of file +tests/NRedisStack.Tests/lcov.info +tests/NRedisStack.Tests/.env +tests/NRedisStack.Tests/redis_ca.pem +tests/NRedisStack.Tests/redis_credentials/redis_user_private.key +tests/NRedisStack.Tests/redis_credentials/redis_user.crt +.env diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index ef37aa63..d95fc2da 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -1,20 +1,32 @@ +using System.Net.Security; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using Moq; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using NRedisStack.Search; using NRedisStack.Search.Aggregation; using NRedisStack.Search.Literals.Enums; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.OpenSsl; using StackExchange.Redis; using Xunit; +using Xunit.Abstractions; using static NRedisStack.Search.Schema; namespace NRedisStack.Tests; public class ExaplesTests : AbstractNRedisStackTest, IDisposable { + private readonly ITestOutputHelper testOutputHelper; Mock _mock = new Mock(); private readonly string key = "EXAMPLES_TESTS"; - public ExaplesTests(RedisFixture redisFixture) : base(redisFixture) { } + public ExaplesTests(RedisFixture redisFixture, ITestOutputHelper testOutputHelper) : base(redisFixture) + { + this.testOutputHelper = testOutputHelper; + } public void Dispose() { @@ -292,6 +304,272 @@ public void TestJsonConvert() Assert.Equal(10, docs.Count()); } +#if NET481 + [Fact] + public void TestRedisCloudConnection_net481() + { + var root = Path.GetFullPath(Directory.GetCurrentDirectory()); + var redisCaPath = Path.GetFullPath(Path.Combine(root, "redis_ca.pem")); + var redisUserCrtPath = Path.GetFullPath(Path.Combine(root, "redis_user.crt")); + var redisUserPrivateKeyPath = Path.GetFullPath(Path.Combine(root, "redis_user_private.key")); + + var password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new Exception("PASSWORD is not set."); + var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? throw new Exception("ENDPOINT is not set."); + + // Load the Redis credentials + var redisUserCertificate = new X509Certificate2(File.ReadAllBytes(redisUserCrtPath)); + var redisCaCertificate = new X509Certificate2(File.ReadAllBytes(redisCaPath)); + + var rsa = RSA.Create(); + + var redisUserPrivateKeyText = File.ReadAllText(redisUserPrivateKeyPath).Trim(); + rsa.ImportParameters(ImportPrivateKey(redisUserPrivateKeyText)); + + var clientCert = redisUserCertificate.CopyWithPrivateKey(rsa); + + // Connect to Redis Cloud + var redisConfiguration = new ConfigurationOptions + { + EndPoints = { endpoint }, + Ssl = true, + Password = password + }; + + redisConfiguration.CertificateSelection += (_, _, _, _, _) => new X509Certificate2(clientCert.Export(X509ContentType.Pfx)); + + redisConfiguration.CertificateValidation += (_, cert, _, errors) => + { + if (errors == SslPolicyErrors.None) + { + return true; + } + + var privateChain = new X509Chain(); + privateChain.ChainPolicy = new X509ChainPolicy { RevocationMode = X509RevocationMode.NoCheck }; + X509Certificate2 cert2 = new X509Certificate2(cert!); + privateChain.ChainPolicy.ExtraStore.Add(redisCaCertificate); + privateChain.Build(cert2); + + bool isValid = true; + + // we're establishing the trust chain so if the only complaint is that that the root CA is untrusted, and the root CA root + // matches our certificate, we know it's ok + foreach (X509ChainStatus chainStatus in privateChain.ChainStatus.Where(x => + x.Status != X509ChainStatusFlags.UntrustedRoot)) + { + if (chainStatus.Status != X509ChainStatusFlags.NoError) + { + isValid = false; + break; + } + } + + return isValid; + }; + + + var redis = ConnectionMultiplexer.Connect(redisConfiguration); + var db = redis.GetDatabase(); + db.Ping(); + } + + public static RSAParameters ImportPrivateKey(string pem) + { + using var sr = new StringReader(pem); + PemReader pr = new PemReader(sr); + RSAParameters rp = new RSAParameters(); + while (sr.Peek() != -1) + { + var privKey = pr.ReadObject() as AsymmetricCipherKeyPair; + if (privKey != null) + { + var pkParamaters = (RsaPrivateCrtKeyParameters)privKey.Private; + rp.Modulus = pkParamaters.Modulus.ToByteArrayUnsigned(); + rp.Exponent = pkParamaters.PublicExponent.ToByteArrayUnsigned(); + rp.P = pkParamaters.P.ToByteArrayUnsigned(); + rp.Q = pkParamaters.Q.ToByteArrayUnsigned(); + rp.D = ConvertRSAParametersField(pkParamaters.Exponent, rp.Modulus.Length); + rp.DP = ConvertRSAParametersField(pkParamaters.DP, rp.P.Length); + rp.DQ = ConvertRSAParametersField(pkParamaters.DQ, rp.Q.Length); + rp.InverseQ = ConvertRSAParametersField(pkParamaters.QInv, rp.Q.Length); + } + else + { + throw new ArgumentException("Pem is malformed and could not be parsed"); + } + } + pr.ReadObject(); + return rp; + } + + private static byte[] ConvertRSAParametersField(BigInteger n, int size) + { + byte[] bs = n.ToByteArrayUnsigned(); + if (bs.Length == size) + return bs; + if (bs.Length > size) + throw new ArgumentException("Specified size too small", "size"); + byte[] padded = new byte[size]; + Array.Copy(bs, 0, padded, size - bs.Length, bs.Length); + return padded; + } +#endif + +#if NET6_0_OR_GREATER + [Fact] + public void TestRedisCloudConnection() + { + var root = Path.GetFullPath(Directory.GetCurrentDirectory()); + var redisCaPath = Path.GetFullPath(Path.Combine(root, "redis_ca.pem")); + var redisUserCrtPath = Path.GetFullPath(Path.Combine(root, "redis_user.crt")); + var redisUserPrivateKeyPath = Path.GetFullPath(Path.Combine(root, "redis_user_private.key")); + + var password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new Exception("PASSWORD is not set."); + var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? throw new Exception("ENDPOINT is not set."); + + // Load the Redis credentials + var redisUserCertificate = new X509Certificate2(File.ReadAllBytes(redisUserCrtPath)); + var redisCaCertificate = new X509Certificate2(File.ReadAllBytes(redisCaPath)); + + var rsa = RSA.Create(); + + var redisUserPrivateKeyText = File.ReadAllText(redisUserPrivateKeyPath); + var pemFileData = File.ReadAllLines(redisUserPrivateKeyPath).Where(x => !x.StartsWith("-")); + var binaryEncoding = Convert.FromBase64String(string.Join(null, pemFileData)); + + rsa.ImportRSAPrivateKey(binaryEncoding, out _); + redisUserCertificate.CopyWithPrivateKey(rsa); + rsa.ImportFromPem(redisUserPrivateKeyText.ToCharArray()); + var clientCert = redisUserCertificate.CopyWithPrivateKey(rsa); + + // Connect to Redis Cloud + var redisConfiguration = new ConfigurationOptions + { + EndPoints = { endpoint }, + Ssl = true, + Password = password + }; + + redisConfiguration.CertificateSelection += (_, _, _, _, _) => clientCert; + + redisConfiguration.CertificateValidation += (_, cert, _, errors) => + { + if (errors == SslPolicyErrors.None) + { + return true; + } + + var privateChain = new X509Chain(); + privateChain.ChainPolicy = new X509ChainPolicy { RevocationMode = X509RevocationMode.NoCheck }; + X509Certificate2 cert2 = new X509Certificate2(cert!); + privateChain.ChainPolicy.ExtraStore.Add(redisCaCertificate); + privateChain.Build(cert2); + + bool isValid = true; + + // we're establishing the trust chain so if the only complaint is that that the root CA is untrusted, and the root CA root + // matches our certificate, we know it's ok + foreach (X509ChainStatus chainStatus in privateChain.ChainStatus.Where(x => + x.Status != X509ChainStatusFlags.UntrustedRoot)) + { + if (chainStatus.Status != X509ChainStatusFlags.NoError) + { + isValid = false; + break; + } + } + + return isValid; + }; + + + var redis = ConnectionMultiplexer.Connect(redisConfiguration); + var db = redis.GetDatabase(); + db.Ping(); + } + + [Fact] + public void TestRedisCloudConnection_DotnetCore3() + { + // Replace this with your own Redis Cloud credentials + var root = Path.GetFullPath(Directory.GetCurrentDirectory()); + var redisCaPath = Path.GetFullPath(Path.Combine(root, "redis_ca.pem")); + var redisUserCrtPath = Path.GetFullPath(Path.Combine(root, "redis_user.crt")); + var redisUserPrivateKeyPath = Path.GetFullPath(Path.Combine(root, "redis_user_private.key")); + + var password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new Exception("PASSWORD is not set."); + var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? throw new Exception("ENDPOINT is not set."); + + // Load the Redis credentials + var redisUserCertificate = new X509Certificate2(File.ReadAllBytes(redisUserCrtPath)); + var redisCaCertificate = new X509Certificate2(File.ReadAllBytes(redisCaPath)); + + var rsa = RSA.Create(); + + var redisUserPrivateKeyText = File.ReadAllText(redisUserPrivateKeyPath); + var pemFileData = File.ReadAllLines(redisUserPrivateKeyPath).Where(x => !x.StartsWith("-")); + var binaryEncoding = Convert.FromBase64String(string.Join(null, pemFileData)); + + rsa.ImportRSAPrivateKey(binaryEncoding, out _); + redisUserCertificate.CopyWithPrivateKey(rsa); + rsa.ImportFromPem(redisUserPrivateKeyText.ToCharArray()); + var clientCert = redisUserCertificate.CopyWithPrivateKey(rsa); + + var sslOptions = new SslClientAuthenticationOptions + { + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + LocalCertificateSelectionCallback = (_, _, _, _, _) => clientCert, + RemoteCertificateValidationCallback = (_, cert, _, errors) => + { + if (errors == SslPolicyErrors.None) + { + return true; + } + + var privateChain = new X509Chain(); + privateChain.ChainPolicy = new X509ChainPolicy { RevocationMode = X509RevocationMode.NoCheck }; + X509Certificate2 cert2 = new X509Certificate2(cert!); + privateChain.ChainPolicy.ExtraStore.Add(redisCaCertificate); + privateChain.Build(cert2); + + bool isValid = true; + + // we're establishing the trust chain so if the only complaint is that that the root CA is untrusted, and the root CA root + // matches our certificate, we know it's ok + foreach (X509ChainStatus chainStatus in privateChain.ChainStatus.Where(x=>x.Status != X509ChainStatusFlags.UntrustedRoot)) + { + if (chainStatus.Status != X509ChainStatusFlags.NoError) + { + isValid = false; + break; + } + } + + return isValid; + }, + TargetHost = endpoint + }; + // Connect to Redis Cloud + var redisConfiguration = new ConfigurationOptions + { + EndPoints = { endpoint }, + Ssl = true, + SslHost = sslOptions.TargetHost, + SslClientAuthenticationOptions = host => sslOptions, + Password = password + }; + + + var redis = ConnectionMultiplexer.Connect(redisConfiguration); + var db = redis.GetDatabase(); + db.Ping(); + + db.StringSet("testKey", "testValue"); + var value = db.StringGet("testKey"); + Assert.Equal("testValue", value); + } +#endif + [Fact] public void BasicJsonExamplesTest() { @@ -1067,4 +1345,4 @@ private static void SortAndCompare(List expectedList, List res) Assert.Equal(expectedList[i], res[i].ToString()); } } -} +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index 55871bf7..bc4184b6 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -19,12 +19,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + +