From b1d76f62c85afe3f4d79e813333225bc40c3ad23 Mon Sep 17 00:00:00 2001 From: Timothy Coleman Date: Fri, 26 Apr 2024 13:40:57 +0100 Subject: [PATCH] Add validation to License --- .../Licensing/LicenseTests.cs | 59 ++++++++++++++++++ src/EventStore.Plugins/Licensing/License.cs | 61 ++++++++++++++++++- 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs diff --git a/src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs b/src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs new file mode 100644 index 0000000..c06b782 --- /dev/null +++ b/src/EventStore.Plugins.Tests/Licensing/LicenseTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using EventStore.Plugins.Licensing; +using Xunit; + +namespace EventStore.Plugins.Tests.Licensing; + +public class LicenseTests { + public static (string PublicKey, string PrivateKey) CreateKeyPair() { + using var rsa = RSA.Create(512); + var publicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); + var privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey()); + return (publicKey, privateKey); + } + + [Fact] + public async Task can_create_and_validate_license() { + var (publicKey, privateKey) = CreateKeyPair(); + + var license = await License.CreateAsync(publicKey, privateKey, new Dictionary() { + { "foo", "bar"}, + }); + + // check repeatedly because of https://github.com/dotnet/runtime/issues/43087 + Assert.True(await license.IsValidAsync(publicKey)); + Assert.True(await license.IsValidAsync(publicKey)); + Assert.True(await license.IsValidAsync(publicKey)); + + Assert.Equal("bar", license.Token.Claims.First(c => c.Type == "foo").Value); + } + + [Fact] + public async Task detects_incorrect_public_key() { + var (publicKey, privateKey) = CreateKeyPair(); + var (publicKey2, _) = CreateKeyPair(); + + var license = await License.CreateAsync(publicKey, privateKey, new Dictionary() { + { "foo", "bar"}, + }); + + Assert.False(await license.IsValidAsync(publicKey2)); + } + + [Fact] + public async Task cannot_create_with_inconsistent_keys() { + var (publicKey, _) = CreateKeyPair(); + var (_, privateKey) = CreateKeyPair(); + + var ex = await Assert.ThrowsAsync(() => + License.CreateAsync(publicKey, privateKey, new Dictionary() { + { "foo", "bar"}, + })); + + Assert.Equal("Token could not be validated", ex.Message); + } +} diff --git a/src/EventStore.Plugins/Licensing/License.cs b/src/EventStore.Plugins/Licensing/License.cs index 8aa37bb..e4d6683 100644 --- a/src/EventStore.Plugins/Licensing/License.cs +++ b/src/EventStore.Plugins/Licensing/License.cs @@ -1,5 +1,62 @@ -using Microsoft.IdentityModel.JsonWebTokens; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using System.Collections.Generic; namespace EventStore.Plugins.Licensing; -public record License(JsonWebToken Token); +public record License(JsonWebToken Token) { + public async Task IsValidAsync(string publicKey) { + var result = await ValidateTokenAsync(publicKey, Token.EncodedToken); + return result.IsValid; + } + + public static async Task CreateAsync( + string publicKey, + string privateKey, + IDictionary claims) { + + using var rsa = RSA.Create(); + rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out _); + var tokenHandler = new JsonWebTokenHandler(); + var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { + Audience = "esdb", + Issuer = "esdb", + Expires = DateTime.UtcNow + TimeSpan.FromHours(1), + Claims = claims, + SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256), + }); + + var result = await ValidateTokenAsync(publicKey, token); + + if (!result.IsValid) + throw new Exception("Token could not be validated"); + + if (result.SecurityToken is not JsonWebToken jwt) + throw new Exception("Token is not a JWT"); + + return new License(jwt); + } + + private static async Task ValidateTokenAsync(string publicKey, string token) { + // not very satisfactory https://github.com/dotnet/runtime/issues/43087 + CryptoProviderFactory.Default.CacheSignatureProviders = false; + + using var rsa = RSA.Create(); + rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _); + var result = await new JsonWebTokenHandler().ValidateTokenAsync( + token, + new TokenValidationParameters { + ValidIssuer = "esdb", + ValidAudience = "esdb", + IssuerSigningKey = new RsaSecurityKey(rsa), + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateIssuer = true, + ValidateLifetime = true, + }); + return result; + } +}