Skip to content

Commit e269de5

Browse files
shacharPashslorello89chayim
authored
Redis Cloud Connection Example (#98)
* TestRedisCloudConnection * using .env for the enviroment vars * DotEnv class * delete 'all' var * add env vars to yml * delete DotEnv class * test TLS Connecting in .NET Core * add to gitignor * Experiments * adding client cert parsing + root CA validation * using env vars * add vars to integration * change to Convert.FromHexString * using files * quoting echo * add files to net7.0 * try #if !WINDOWS * wrap all * delete usings * adding bouncyCastle+4.8.1 example * adding back usings * echoing vars to files * same pattern for windows * try different way * Save test certificates with wsl-bash * trying * delete mkdir line * one run try * return back * add .env to gitignor * change ImportPrivateKey * return to how it was before * comment repeated lines * fixing 4.8.1 tests * adding some light debugging * could it be? * adding trim * removing debugging --------- Co-authored-by: slorello89 <[email protected]> Co-authored-by: Chayim I. Kirshen <[email protected]> Co-authored-by: slorello89 <[email protected]>
1 parent 74b0076 commit e269de5

File tree

4 files changed

+318
-6
lines changed

4 files changed

+318
-6
lines changed

.github/workflows/integration.yml

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ jobs:
1818
build_and_Test:
1919
name: Build and test
2020
runs-on: ubuntu-latest
21+
environment: REDIS_USER
22+
env:
23+
USER_NAME: ${{ secrets.USER_NAME }}
24+
PASSWORD: ${{ secrets.PASSWORD }}
25+
ENDPOINT: ${{ secrets.ENDPOINT }}
2126
steps:
2227
- uses: actions/checkout@v3
2328
- name: .NET Core 6
@@ -35,9 +40,19 @@ jobs:
3540
- name: Build
3641
run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true
3742
- name: Test
38-
run: dotnet test -f net6.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
43+
run: |
44+
echo "${{secrets.REDIS_CA_PEM}}" > tests/NRedisStack.Tests/bin/Debug/net6.0/redis_ca.pem
45+
echo "${{secrets.REDIS_USER_CRT}}" > tests/NRedisStack.Tests/bin/Debug/net6.0/redis_user.crt
46+
echo "${{secrets.REDIS_USER_PRIVATE_KEY}}" > tests/NRedisStack.Tests/bin/Debug/net6.0/redis_user_private.key
47+
ls -R
48+
dotnet test -f net6.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
3949
- name: Test
40-
run: dotnet test -f net7.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
50+
run: |
51+
echo "${{secrets.REDIS_CA_PEM}}" > tests/NRedisStack.Tests/bin/Debug/net7.0/redis_ca.pem
52+
echo "${{secrets.REDIS_USER_CRT}}" > tests/NRedisStack.Tests/bin/Debug/net7.0/redis_user.crt
53+
echo "${{secrets.REDIS_USER_PRIVATE_KEY}}" > tests/NRedisStack.Tests/bin/Debug/net7.0/redis_user_private.key
54+
ls -R
55+
dotnet test -f net7.0 --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
4156
- name: Codecov
4257
uses: codecov/codecov-action@v3
4358
with:
@@ -48,6 +63,11 @@ jobs:
4863
build_and_test_windows:
4964
name: Build and Test on Windows
5065
runs-on: windows-latest
66+
environment: REDIS_USER
67+
env:
68+
USER_NAME: ${{ secrets.USER_NAME }}
69+
PASSWORD: ${{ secrets.PASSWORD }}
70+
ENDPOINT: ${{ secrets.ENDPOINT }}
5171
steps:
5272
- uses: actions/checkout@v3
5373
- uses: Vampire/setup-wsl@v2
@@ -59,11 +79,18 @@ jobs:
5979
sudo apt-get update
6080
sudo apt-get install curl -y && sudo apt-get install gpg -y && apt-get install lsb-release -y && apt-get install libgomp1 -y
6181
curl https://packages.redis.io/redis-stack/redis-stack-server-${{env.redis_stack_version}}.jammy.x86_64.tar.gz -o redis-stack.tar.gz
62-
tar xf redis-stack.tar.gz
82+
tar xf redis-stack.tar.gz
6383
- name: Restore dependencies
6484
run: dotnet restore
6585
- name: Build
6686
run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true
87+
- name: Save test certificates
88+
shell: wsl-bash {0}
89+
run: |
90+
echo "${{secrets.REDIS_CA_PEM}}" > tests/NRedisStack.Tests/bin/Debug/net481/redis_ca.pem
91+
echo "${{secrets.REDIS_USER_CRT}}" > tests/NRedisStack.Tests/bin/Debug/net481/redis_user.crt
92+
echo "${{secrets.REDIS_USER_PRIVATE_KEY}}" > tests/NRedisStack.Tests/bin/Debug/net481/redis_user_private.key
93+
ls -R
6794
- name: Test
6895
shell: cmd
6996
run: |

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,9 @@ FodyWeavers.xsd
399399
.idea
400400
tests/NRedisStack.Tests/lcov.net7.0.info
401401
tests/NRedisStack.Tests/lcov.net6.0.info
402-
tests/NRedisStack.Tests/lcov.info
402+
tests/NRedisStack.Tests/lcov.info
403+
tests/NRedisStack.Tests/.env
404+
tests/NRedisStack.Tests/redis_ca.pem
405+
tests/NRedisStack.Tests/redis_credentials/redis_user_private.key
406+
tests/NRedisStack.Tests/redis_credentials/redis_user.crt
407+
.env

tests/NRedisStack.Tests/Examples/ExamplesTests.cs

Lines changed: 280 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1+
using System.Net.Security;
2+
using System.Security.Cryptography;
3+
using System.Security.Cryptography.X509Certificates;
14
using Moq;
25
using NRedisStack.DataTypes;
36
using NRedisStack.RedisStackCommands;
47
using NRedisStack.Search;
58
using NRedisStack.Search.Aggregation;
69
using NRedisStack.Search.Literals.Enums;
10+
using Org.BouncyCastle.Crypto;
11+
using Org.BouncyCastle.Crypto.Parameters;
12+
using Org.BouncyCastle.Math;
13+
using Org.BouncyCastle.OpenSsl;
714
using StackExchange.Redis;
815
using Xunit;
16+
using Xunit.Abstractions;
917
using static NRedisStack.Search.Schema;
1018

1119
namespace NRedisStack.Tests;
1220

1321
public class ExaplesTests : AbstractNRedisStackTest, IDisposable
1422
{
23+
private readonly ITestOutputHelper testOutputHelper;
1524
Mock<IDatabase> _mock = new Mock<IDatabase>();
1625
private readonly string key = "EXAMPLES_TESTS";
17-
public ExaplesTests(RedisFixture redisFixture) : base(redisFixture) { }
26+
public ExaplesTests(RedisFixture redisFixture, ITestOutputHelper testOutputHelper) : base(redisFixture)
27+
{
28+
this.testOutputHelper = testOutputHelper;
29+
}
1830

1931
public void Dispose()
2032
{
@@ -292,6 +304,272 @@ public void TestJsonConvert()
292304
Assert.Equal(10, docs.Count());
293305
}
294306

307+
#if NET481
308+
[Fact]
309+
public void TestRedisCloudConnection_net481()
310+
{
311+
var root = Path.GetFullPath(Directory.GetCurrentDirectory());
312+
var redisCaPath = Path.GetFullPath(Path.Combine(root, "redis_ca.pem"));
313+
var redisUserCrtPath = Path.GetFullPath(Path.Combine(root, "redis_user.crt"));
314+
var redisUserPrivateKeyPath = Path.GetFullPath(Path.Combine(root, "redis_user_private.key"));
315+
316+
var password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new Exception("PASSWORD is not set.");
317+
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? throw new Exception("ENDPOINT is not set.");
318+
319+
// Load the Redis credentials
320+
var redisUserCertificate = new X509Certificate2(File.ReadAllBytes(redisUserCrtPath));
321+
var redisCaCertificate = new X509Certificate2(File.ReadAllBytes(redisCaPath));
322+
323+
var rsa = RSA.Create();
324+
325+
var redisUserPrivateKeyText = File.ReadAllText(redisUserPrivateKeyPath).Trim();
326+
rsa.ImportParameters(ImportPrivateKey(redisUserPrivateKeyText));
327+
328+
var clientCert = redisUserCertificate.CopyWithPrivateKey(rsa);
329+
330+
// Connect to Redis Cloud
331+
var redisConfiguration = new ConfigurationOptions
332+
{
333+
EndPoints = { endpoint },
334+
Ssl = true,
335+
Password = password
336+
};
337+
338+
redisConfiguration.CertificateSelection += (_, _, _, _, _) => new X509Certificate2(clientCert.Export(X509ContentType.Pfx));
339+
340+
redisConfiguration.CertificateValidation += (_, cert, _, errors) =>
341+
{
342+
if (errors == SslPolicyErrors.None)
343+
{
344+
return true;
345+
}
346+
347+
var privateChain = new X509Chain();
348+
privateChain.ChainPolicy = new X509ChainPolicy { RevocationMode = X509RevocationMode.NoCheck };
349+
X509Certificate2 cert2 = new X509Certificate2(cert!);
350+
privateChain.ChainPolicy.ExtraStore.Add(redisCaCertificate);
351+
privateChain.Build(cert2);
352+
353+
bool isValid = true;
354+
355+
// we're establishing the trust chain so if the only complaint is that that the root CA is untrusted, and the root CA root
356+
// matches our certificate, we know it's ok
357+
foreach (X509ChainStatus chainStatus in privateChain.ChainStatus.Where(x =>
358+
x.Status != X509ChainStatusFlags.UntrustedRoot))
359+
{
360+
if (chainStatus.Status != X509ChainStatusFlags.NoError)
361+
{
362+
isValid = false;
363+
break;
364+
}
365+
}
366+
367+
return isValid;
368+
};
369+
370+
371+
var redis = ConnectionMultiplexer.Connect(redisConfiguration);
372+
var db = redis.GetDatabase();
373+
db.Ping();
374+
}
375+
376+
public static RSAParameters ImportPrivateKey(string pem)
377+
{
378+
using var sr = new StringReader(pem);
379+
PemReader pr = new PemReader(sr);
380+
RSAParameters rp = new RSAParameters();
381+
while (sr.Peek() != -1)
382+
{
383+
var privKey = pr.ReadObject() as AsymmetricCipherKeyPair;
384+
if (privKey != null)
385+
{
386+
var pkParamaters = (RsaPrivateCrtKeyParameters)privKey.Private;
387+
rp.Modulus = pkParamaters.Modulus.ToByteArrayUnsigned();
388+
rp.Exponent = pkParamaters.PublicExponent.ToByteArrayUnsigned();
389+
rp.P = pkParamaters.P.ToByteArrayUnsigned();
390+
rp.Q = pkParamaters.Q.ToByteArrayUnsigned();
391+
rp.D = ConvertRSAParametersField(pkParamaters.Exponent, rp.Modulus.Length);
392+
rp.DP = ConvertRSAParametersField(pkParamaters.DP, rp.P.Length);
393+
rp.DQ = ConvertRSAParametersField(pkParamaters.DQ, rp.Q.Length);
394+
rp.InverseQ = ConvertRSAParametersField(pkParamaters.QInv, rp.Q.Length);
395+
}
396+
else
397+
{
398+
throw new ArgumentException("Pem is malformed and could not be parsed");
399+
}
400+
}
401+
pr.ReadObject();
402+
return rp;
403+
}
404+
405+
private static byte[] ConvertRSAParametersField(BigInteger n, int size)
406+
{
407+
byte[] bs = n.ToByteArrayUnsigned();
408+
if (bs.Length == size)
409+
return bs;
410+
if (bs.Length > size)
411+
throw new ArgumentException("Specified size too small", "size");
412+
byte[] padded = new byte[size];
413+
Array.Copy(bs, 0, padded, size - bs.Length, bs.Length);
414+
return padded;
415+
}
416+
#endif
417+
418+
#if NET6_0_OR_GREATER
419+
[Fact]
420+
public void TestRedisCloudConnection()
421+
{
422+
var root = Path.GetFullPath(Directory.GetCurrentDirectory());
423+
var redisCaPath = Path.GetFullPath(Path.Combine(root, "redis_ca.pem"));
424+
var redisUserCrtPath = Path.GetFullPath(Path.Combine(root, "redis_user.crt"));
425+
var redisUserPrivateKeyPath = Path.GetFullPath(Path.Combine(root, "redis_user_private.key"));
426+
427+
var password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new Exception("PASSWORD is not set.");
428+
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? throw new Exception("ENDPOINT is not set.");
429+
430+
// Load the Redis credentials
431+
var redisUserCertificate = new X509Certificate2(File.ReadAllBytes(redisUserCrtPath));
432+
var redisCaCertificate = new X509Certificate2(File.ReadAllBytes(redisCaPath));
433+
434+
var rsa = RSA.Create();
435+
436+
var redisUserPrivateKeyText = File.ReadAllText(redisUserPrivateKeyPath);
437+
var pemFileData = File.ReadAllLines(redisUserPrivateKeyPath).Where(x => !x.StartsWith("-"));
438+
var binaryEncoding = Convert.FromBase64String(string.Join(null, pemFileData));
439+
440+
rsa.ImportRSAPrivateKey(binaryEncoding, out _);
441+
redisUserCertificate.CopyWithPrivateKey(rsa);
442+
rsa.ImportFromPem(redisUserPrivateKeyText.ToCharArray());
443+
var clientCert = redisUserCertificate.CopyWithPrivateKey(rsa);
444+
445+
// Connect to Redis Cloud
446+
var redisConfiguration = new ConfigurationOptions
447+
{
448+
EndPoints = { endpoint },
449+
Ssl = true,
450+
Password = password
451+
};
452+
453+
redisConfiguration.CertificateSelection += (_, _, _, _, _) => clientCert;
454+
455+
redisConfiguration.CertificateValidation += (_, cert, _, errors) =>
456+
{
457+
if (errors == SslPolicyErrors.None)
458+
{
459+
return true;
460+
}
461+
462+
var privateChain = new X509Chain();
463+
privateChain.ChainPolicy = new X509ChainPolicy { RevocationMode = X509RevocationMode.NoCheck };
464+
X509Certificate2 cert2 = new X509Certificate2(cert!);
465+
privateChain.ChainPolicy.ExtraStore.Add(redisCaCertificate);
466+
privateChain.Build(cert2);
467+
468+
bool isValid = true;
469+
470+
// we're establishing the trust chain so if the only complaint is that that the root CA is untrusted, and the root CA root
471+
// matches our certificate, we know it's ok
472+
foreach (X509ChainStatus chainStatus in privateChain.ChainStatus.Where(x =>
473+
x.Status != X509ChainStatusFlags.UntrustedRoot))
474+
{
475+
if (chainStatus.Status != X509ChainStatusFlags.NoError)
476+
{
477+
isValid = false;
478+
break;
479+
}
480+
}
481+
482+
return isValid;
483+
};
484+
485+
486+
var redis = ConnectionMultiplexer.Connect(redisConfiguration);
487+
var db = redis.GetDatabase();
488+
db.Ping();
489+
}
490+
491+
[Fact]
492+
public void TestRedisCloudConnection_DotnetCore3()
493+
{
494+
// Replace this with your own Redis Cloud credentials
495+
var root = Path.GetFullPath(Directory.GetCurrentDirectory());
496+
var redisCaPath = Path.GetFullPath(Path.Combine(root, "redis_ca.pem"));
497+
var redisUserCrtPath = Path.GetFullPath(Path.Combine(root, "redis_user.crt"));
498+
var redisUserPrivateKeyPath = Path.GetFullPath(Path.Combine(root, "redis_user_private.key"));
499+
500+
var password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new Exception("PASSWORD is not set.");
501+
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? throw new Exception("ENDPOINT is not set.");
502+
503+
// Load the Redis credentials
504+
var redisUserCertificate = new X509Certificate2(File.ReadAllBytes(redisUserCrtPath));
505+
var redisCaCertificate = new X509Certificate2(File.ReadAllBytes(redisCaPath));
506+
507+
var rsa = RSA.Create();
508+
509+
var redisUserPrivateKeyText = File.ReadAllText(redisUserPrivateKeyPath);
510+
var pemFileData = File.ReadAllLines(redisUserPrivateKeyPath).Where(x => !x.StartsWith("-"));
511+
var binaryEncoding = Convert.FromBase64String(string.Join(null, pemFileData));
512+
513+
rsa.ImportRSAPrivateKey(binaryEncoding, out _);
514+
redisUserCertificate.CopyWithPrivateKey(rsa);
515+
rsa.ImportFromPem(redisUserPrivateKeyText.ToCharArray());
516+
var clientCert = redisUserCertificate.CopyWithPrivateKey(rsa);
517+
518+
var sslOptions = new SslClientAuthenticationOptions
519+
{
520+
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
521+
LocalCertificateSelectionCallback = (_, _, _, _, _) => clientCert,
522+
RemoteCertificateValidationCallback = (_, cert, _, errors) =>
523+
{
524+
if (errors == SslPolicyErrors.None)
525+
{
526+
return true;
527+
}
528+
529+
var privateChain = new X509Chain();
530+
privateChain.ChainPolicy = new X509ChainPolicy { RevocationMode = X509RevocationMode.NoCheck };
531+
X509Certificate2 cert2 = new X509Certificate2(cert!);
532+
privateChain.ChainPolicy.ExtraStore.Add(redisCaCertificate);
533+
privateChain.Build(cert2);
534+
535+
bool isValid = true;
536+
537+
// we're establishing the trust chain so if the only complaint is that that the root CA is untrusted, and the root CA root
538+
// matches our certificate, we know it's ok
539+
foreach (X509ChainStatus chainStatus in privateChain.ChainStatus.Where(x=>x.Status != X509ChainStatusFlags.UntrustedRoot))
540+
{
541+
if (chainStatus.Status != X509ChainStatusFlags.NoError)
542+
{
543+
isValid = false;
544+
break;
545+
}
546+
}
547+
548+
return isValid;
549+
},
550+
TargetHost = endpoint
551+
};
552+
// Connect to Redis Cloud
553+
var redisConfiguration = new ConfigurationOptions
554+
{
555+
EndPoints = { endpoint },
556+
Ssl = true,
557+
SslHost = sslOptions.TargetHost,
558+
SslClientAuthenticationOptions = host => sslOptions,
559+
Password = password
560+
};
561+
562+
563+
var redis = ConnectionMultiplexer.Connect(redisConfiguration);
564+
var db = redis.GetDatabase();
565+
db.Ping();
566+
567+
db.StringSet("testKey", "testValue");
568+
var value = db.StringGet("testKey");
569+
Assert.Equal("testValue", value);
570+
}
571+
#endif
572+
295573
[Fact]
296574
public void BasicJsonExamplesTest()
297575
{
@@ -1067,4 +1345,4 @@ private static void SortAndCompare(List<string> expectedList, List<string> res)
10671345
Assert.Equal(expectedList[i], res[i].ToString());
10681346
}
10691347
}
1070-
}
1348+
}

0 commit comments

Comments
 (0)