From 14e878bd243aff245e9b9c5804e1d29c166f3a52 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:04:20 -0400 Subject: [PATCH 01/13] Work On Licensing --- .../src/Bitwarden.Extensions.Hosting.csproj | 2 + .../src/HostBuilderExtensions.cs | 13 ++ .../src/Licensing/DefaultLicensingService.cs | 93 ++++++++ .../src/Licensing/ILicensingService.cs | 35 +++ .../src/Licensing/InvalidLicenseException.cs | 66 ++++++ .../src/Licensing/LicensingOptions.cs | 63 +++++ .../PostConfigureLicensingOptions.cs | 146 ++++++++++++ .../Bitwarden.Extensions.Hosting.Tests.csproj | 1 + .../Licensing/DefaultLicensingServiceTests.cs | 221 ++++++++++++++++++ .../PostConfigureLicensingOptionsTests.cs | 41 ++++ 10 files changed, 681 insertions(+) create mode 100644 extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs create mode 100644 extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs create mode 100644 extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs create mode 100644 extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs create mode 100644 extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs create mode 100644 extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs create mode 100644 extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj b/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj index 6980999c..04a35a52 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj +++ b/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj @@ -24,6 +24,7 @@ + @@ -31,6 +32,7 @@ + diff --git a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs index 7612f5b2..0bad5911 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs @@ -2,11 +2,13 @@ using System.Reflection; using Bitwarden.Extensions.Hosting; using Bitwarden.Extensions.Hosting.Features; +using Bitwarden.Extensions.Hosting.Licensing; using LaunchDarkly.Sdk.Server.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Serilog; @@ -74,6 +76,7 @@ public static TBuilder UseBitwardenDefaults(this TBuilder builder, Bit } AddFeatureFlagServices(builder.Services, builder.Configuration); + AddLicensingServices(builder.Services, builder.Configuration); return builder; } @@ -133,6 +136,7 @@ public static IHostBuilder UseBitwardenDefaults(this IHostBuilder hostBuilder, B hostBuilder.ConfigureServices((context, services) => { AddFeatureFlagServices(services, context.Configuration); + AddLicensingServices(services, context.Configuration); }); return hostBuilder; @@ -241,4 +245,13 @@ private static void AddFeatureFlagServices(IServiceCollection services, IConfigu services.TryAddScoped(sp => sp.GetRequiredService().Get()); services.TryAddScoped(); } + + private static void AddLicensingServices(IServiceCollection services, IConfiguration configuration) + { + services.TryAddEnumerable( + ServiceDescriptor.Singleton, PostConfigureLicensingOptions>() + ); + + services.Configure(configuration.GetSection("Licensing")); + } } diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs new file mode 100644 index 00000000..47ce98a0 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs @@ -0,0 +1,93 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Bitwarden.Extensions.Hosting.Licensing; + +internal sealed class DefaultLicensingService : ILicensingService +{ + private readonly LicensingOptions _licensingOptions; + private readonly TimeProvider _timeProvider; + + public DefaultLicensingService(IOptions licensingOptions, TimeProvider timeProvider) + { + ArgumentNullException.ThrowIfNull(licensingOptions); + + // TODO: Do we need to support runtime changes to these settings at all, I don't think we do... + _licensingOptions = licensingOptions.Value; + _timeProvider = timeProvider; + + // We are cloud if the signing certificate has a private key that can sign licenses and local development + // hasn't forced self host. + IsCloud = _licensingOptions.SigningCertificate.HasPrivateKey && !_licensingOptions.ForceSelfHost; + } + + public bool IsCloud { get; } + + public string CreateLicense(IEnumerable claims, TimeSpan validFor) + { + ArgumentNullException.ThrowIfNull(claims); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(validFor, TimeSpan.Zero); + + if (!IsCloud) + { + throw new InvalidOperationException("Self-hosted services can not create a license, please check 'IsCloud' before calling this method."); + } + + var now = _timeProvider.GetUtcNow().UtcDateTime; + + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + // Issuer = "bitwarden.com", // TODO: Is this what we want? + // Audience = "configurable_product_name??", // TODO: Is this what we want? + SigningCredentials = new SigningCredentials( + new X509SecurityKey(_licensingOptions.SigningCertificate), SecurityAlgorithms.RsaSha256), + IssuedAt = now, + NotBefore = now, + Expires = now.Add(validFor), + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + + var token = tokenHandler.CreateToken(tokenDescriptor); + + return tokenHandler.WriteToken(token); + } + + public async Task> VerifyLicenseAsync(string license) + { + ArgumentNullException.ThrowIfNull(license); + // TODO: Should we validate that this is self host? + // It's not technically wrong to be able to do that but we don't do it currently + // so we could disallow it. + + var tokenHandler = new JwtSecurityTokenHandler(); + + if (!tokenHandler.CanReadToken(license)) + { + throw new InvalidLicenseException(InvalidLicenseReason.InvalidFormat); + } + + var tokenValidateParameters = new TokenValidationParameters + { + IssuerSigningKey = new X509SecurityKey(_licensingOptions.SigningCertificate), + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ValidateIssuer = false, // TODO: Do we want to have no issuer? + ValidateAudience = false, // TODO: Do we want to have no audience? + }; + + var tokenValidationResult = await tokenHandler.ValidateTokenAsync(license, tokenValidateParameters); + + if (!tokenValidationResult.IsValid) + { + throw tokenValidationResult.Exception; + } + + // Should I even take a ClaimsIdentity and return it here instead of a list of claims? + return tokenValidationResult.ClaimsIdentity.Claims; + } +} diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs new file mode 100644 index 00000000..a2e385b8 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs @@ -0,0 +1,35 @@ +using System.Security.Claims; + +namespace Bitwarden.Extensions.Hosting.Licensing; + +/// +/// A service with the ability to consume and create licenses. +/// +public interface ILicensingService +{ + /// + /// Returns whether or not the current service is running as a cloud instance. + /// + bool IsCloud { get; } + + // TODO: Any other options than valid for? + /// + /// Creates a signed license that can be consumed on self-hosted instances. + /// + /// + /// This method can only be called when returns . + /// + /// The claims to include in the license file. + /// How long the license should be valid for. + /// + /// A string representation of the license that can be given to people to store with their self hosted instance. + /// + string CreateLicense(IEnumerable claims, TimeSpan validFor); + + /// + /// Verifies that the given license is valid and can have it's contents be trusted. + /// + /// The license to check. + /// An enumerable of claims included in the license. + Task> VerifyLicenseAsync(string license); +} diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs new file mode 100644 index 00000000..d48129c8 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs @@ -0,0 +1,66 @@ +namespace Bitwarden.Extensions.Hosting.Licensing; + +/// +/// A set of reasons explaining why a license was invalid. +/// +public enum InvalidLicenseReason +{ + /// + /// The given license was in an invalid format and could not be read further. + /// + InvalidFormat, + + /// + /// The given license may have been valid previously but has expired and should no longer be used. + /// + Expired, + + /// + /// The license is invalid for an unknown reason, checks logs for additional details. + /// + Unknown, +} + +/// +/// The exception that is thrown when a license is invalid and cannot be verified. +/// +public class InvalidLicenseException : Exception +{ + private const string DefaultMessage = "The license is invalid and cannot be trusted."; + + /// + /// Initializes a new instance of . + /// + /// + public InvalidLicenseException(InvalidLicenseReason reason) + : base(DefaultMessage) + { + Reason = reason; + } + + /// + /// Initializes a new instance of . + /// + /// + /// + public InvalidLicenseException(InvalidLicenseReason reason, string? message) + : this(reason, message, null) + { } + + /// + /// Initializes a new instance of . + /// + /// + /// + /// + public InvalidLicenseException(InvalidLicenseReason reason, string? message, Exception? innerException) + : base(message ?? DefaultMessage, innerException) + { + Reason = reason; + } + + /// + /// The reason the license was found to be invalid. + /// + public InvalidLicenseReason Reason { get; } +} diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs new file mode 100644 index 00000000..e50ac572 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs @@ -0,0 +1,63 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Bitwarden.Extensions.Hosting.Licensing; + +/// +/// A set of options for customizing how licensing behaves. +/// +public sealed class LicensingOptions +{ + /// + /// Options for configuring license retrieval from azure blob storage. + /// + public AzureBlobLicensingOptions AzureBlob { get; set; } = new AzureBlobLicensingOptions(); + + /// + /// The certificate that will be used to either sign or validate licenses. + /// + public X509Certificate2 SigningCertificate { get; set; } = null!; + + /// + /// Development option to force the usage of self hosted organization even though a certificate + /// that can sign licenses it available. + /// + public bool ForceSelfHost { get; set; } +} + +/// +/// A set of options for customizing how to find a certificate in Azure blob storage. +/// +public sealed class AzureBlobLicensingOptions +{ + /// + /// The connection string to the azure blob storage account. + /// + public string? ConnectionString { get; set; } + + /// + /// The password for the certificate stored in azure blob storage. + /// + public string? CertificatePassword { get; set; } + + /// + /// The name of the blob the certificate is stored in, defaults to certificates. + /// + public string BlobName { get; set; } = "certificates"; + + /// + /// The name of the license stored in azure blob storage, defaults to licensing.pfx. + /// + public string LicenseName { get; set; } = "licensing.pfx"; +} + +/// +/// It's important that these can't be set through configuration, only through code. +/// +/// +/// We would need to make this public for services to customize this. +/// +internal sealed class InternalLicensingOptions +{ + public string DevelopmentThumbprint { get; set; } = "207E64A231E8AA32AAF68A61037C075EBEBD553F"; + public string NonDevelopmentThumbprint { get; set; } = "B34876439FCDA2846505B2EFBBA6C4A951313EBE"; +} diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs new file mode 100644 index 00000000..534be9f7 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs @@ -0,0 +1,146 @@ +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; + +namespace Bitwarden.Extensions.Hosting.Licensing; + +internal sealed class PostConfigureLicensingOptions : IPostConfigureOptions +{ + private readonly InternalLicensingOptions _internalLicensingOptions; + private readonly ILogger _logger; + private readonly IHostEnvironment _hostEnvironment; + + public PostConfigureLicensingOptions( + IOptions internalLicensingOptions, + ILogger logger, + IHostEnvironment hostEnvironment) + { + _internalLicensingOptions = internalLicensingOptions.Value; + _logger = logger; + _hostEnvironment = hostEnvironment; + } + + public void PostConfigure(string? name, LicensingOptions options) + { + if (name != Options.DefaultName) + { + return; + } + + if (options.SigningCertificate != null) + { + // Something already set it, no problem, return early. + return; + } + + void DoFinalValidation() + { + var signingCertificate = options.SigningCertificate; + + if (signingCertificate == null) + { + throw new InvalidOperationException("No signing certificate could be retrieved."); + } + + // TODO: Validate thumbprint one last time + + if (options.ForceSelfHost && !_hostEnvironment.IsDevelopment()) + { + // Force self host is only allowed when running as development + options.ForceSelfHost = false; + } + } + + + // Try Azure Blob first + if (TryAzureBlob(options)) + { + DoFinalValidation(); + return; + } + + + // Try Cert store + if (TryCertStore(options)) + { + DoFinalValidation(); + return; + } + + + // Try Assembly embedded + if (TryGetEmbeddedCert(options)) + { + DoFinalValidation(); + return; + } + + // TODO: Throw good exception + throw new InvalidOperationException("Signing certificate could not be attained."); + } + + private bool TryAzureBlob(LicensingOptions options) + { + if (string.IsNullOrEmpty(options.AzureBlob.ConnectionString)) + { + return false; + } + + // Infer them as trying to do azure blob + if (string.IsNullOrEmpty(options.AzureBlob.CertificatePassword)) + { + // TODO: Use logger generator + _logger.LogWarning("An Azure Blob connection string but not a certificate password -- did you miss something?"); + return false; + } + + // TODO: Do azure blob stuff + // TODO: Set SigningCertificate + return true; + } + + private bool TryCertStore(LicensingOptions options) + { + var thumbprint = _hostEnvironment.IsDevelopment() + ? _internalLicensingOptions.DevelopmentThumbprint + : _internalLicensingOptions.NonDevelopmentThumbprint; + + using var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + certStore.Open(OpenFlags.ReadOnly); + var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (certCollection.Count == 0) + { + return false; + } + + options.SigningCertificate = certCollection[0]; + return true; + } + + private bool TryGetEmbeddedCert(LicensingOptions options) + { + try + { + var appAssembly = Assembly.Load(new AssemblyName(_hostEnvironment.ApplicationName)); + // TODO: Make cert name configurable through internal options? + var certName = _hostEnvironment.IsDevelopment() + ? "licensing_dev.cer" + : "licensing.cer"; + + var resourceName = appAssembly.GetManifestResourceNames().Single(n => n.EndsWith(certName)); + + using var resourceStream = appAssembly.GetManifestResourceStream(resourceName)!; + using var memoryStream = new MemoryStream(); + resourceStream.CopyTo(memoryStream); + options.SigningCertificate = new X509Certificate2(memoryStream.ToArray()); + return true; + } + catch (FileNotFoundException) + { + // TODO: Log warning + return false; + } + } +} diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj b/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj index 55682400..2977412d 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs new file mode 100644 index 00000000..d3714396 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -0,0 +1,221 @@ +using System.Text; +using System.Security.Cryptography.X509Certificates; +using System.Security.Claims; +using Bitwarden.Extensions.Hosting.Licensing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using Xunit.Abstractions; + +namespace Bitwarden.Extensions.Hosting.Tests.Licensing; + +public class DefaultLicensingServiceTests +{ + private readonly ITestOutputHelper _outputHelper; + private readonly FakeTimeProvider _fakeTimeProvider; + + public DefaultLicensingServiceTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + } + + // Created using: + // openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout test.key -out test.crt -subj "/CN=LicenseTest" -days 3650 + // openssl pkcs12 -export -legacy -out test.pfx -inkey test.key -in test.crt -certfile test.crt + // Password: PfxPassword + // Thumbprint: AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7 + private static X509Certificate2 TestCertificateWithPrivateKey = new X509Certificate2(Convert.FromBase64String(@" +MIIPywIBAzCCD4cGCSqGSIb3DQEHAaCCD3gEgg90MIIPcDCCCbkGCSqGSIb3DQEH +AaCCCaoEggmmMIIJojCCCZ4GCyqGSIb3DQEMCgECoIIJdjCCCXIwJAYKKoZIhvcN +AQwBAzAWBBCVPX0e9RbyA5c11lIfilqwAgIH0ASCCUjpZNP0mNeFdtLLOR1von4z +CkhwozAkc6Wd2CTsOwnBR+FwcHvuEioKT/0bM2f/uxPl25ohvCi1DAAc3DOH2uYJ +aAaTcEzkIf4OoIGE2eYhkMDL0alv3OrrsMvAFJZwXpxiWKWskswsZSjted9GX4eV +XGRRVyJ/jVIS5fyMDORYf3l8xT+X0u6ridXsuxvl6ZAm9w16FC4kbnMmzhP6+Yk3 +SzoT292w8eBtxb16OzzhS1Q+2AJ9/FN2Fcx1xxv0MT9zCJTMu/7b3Bj6efZtl2Oo +hRmxKbsqes29Ow9HZsgObGipKwq5JRtI98Q0SmIRxd+iqaf/X6mME1W+g/HjWAIF +5iD25n4zpngH4Zm+a4Q3HZ7kRu1R3BRS6mkvHI8dL0enFJVm/sVO3U0gYkVt+P8A +WSiLxWe0VbZ07Ihh+GwK8frrftZfNFL8cQfafo90cijZe4esp1S71GYuQMyl35Mg +0BezlqwlvosN1t7OBEJehmTPfh5LzEqaA1Zb67GqGNABBqG9qd9OMVgRdR43tYXi +awOv28nazd7uLfx1RY0zHDyisucG+y/fB8Zx+Yw1DCM0lFdfy9jCX7rgLykccNNA +JFUnPN+MiWTvxJ5qlaK10CCK6LGOgK0sSLTnN2Y81R/0Ne5bcXIn069hCP9HEHrU +5uPDGG1P3ALL82oL6ik/VkWV6qb9NBEBwFGkmkdnchE4nSY9IIm6AMZTxKWs8q7x +pVOsDQszV025BzXGhqmdn3h9x96j4UR/kpsxN3Ni5Dup4vmmhe08q/m1xJLlDNnu +Wzi4ELV9UPQHaj/jR26dckasgTaHrir3FK5e1HuJMxvxrgXhaVmqgA1Bchnid7eU +fbuySYDFf+Jy7YXyAOZtz5mKSgDyRXWzIcGdR5zAzMVaHJ2FrfE61+U51V0Jllt5 +0e4+qL4QoB3JLI/wGiyAvy/u+gNyAulWMwbeyeS3055DT53mu2FRnTRHfX4vUf8B +lyDAYJxPj49HFHv1xeegRU9gwZPG1c/iVbZ4FyBj1vlQGFfFEGOc0J166iDqlwjp +c3LPzOxysXKLxMeU8oL2AxAsnJN4FDBIzOLYPYl6uB+HugGYr2zQGg3iTNCXf5SM +MtfGh2Uzl9SGsa/mU6usLXXXw5j9PuwrqURQtt+r0yYKoYoIp4UpSI27e0y990oP +7mOy1H9KJpAstWoMH+OTIQTkBvAotZL5OXdXdhDSIvm+BgAL5w2agW5IctYbAbi8 +uAeCYcqgESmSDnBu/sPuzmWsWo8fu45uBAAkflmFaF6PWqzAL3MddLHpgcZk4u+c +5DlqrGrRQSGLBzB0naOr4KkKoGFlBdSwJRBAUiXkHUVIW/swMaW67IgOWx0nq9GQ +aXoMMPnQWgX4DIobzGnqCx8Df+ch7M4pwzJqIh4gc3lOyf1eT42Y7WMrd44AOgW/ +/pVuY3SqIlSoZjZJmrdsRsrFpyBIp4ghr6KjisZrEDFHSnAmk+6cGwDbA9fuFSKO +hYKcjM/nOpyWvKnCTCRWk1TbCghF6tb2m45sOJZzz1UFO+38GsAwSgL3JRQtxTOq +444UmNJvIeZaY1CC39ci5U7z+RgIZk74UkBqQOn3LZ9uCRUhp6GHHfJqu8S1Dp22 +EIYsY7Y7sa6MHDbX10X3EFJMYhvloZg6/7TbtGoa+muLe+mVAcCpXcuzeoPkULGw +/QY0YPWvdDBf0xNNMhsCn4cz5rDBlhScXCiC7UoPtlrF2ZPpiONGohFTT0CknhYb +UsA+52W3cMYrnoanZn6zPHrxL0wAf2RfG9C/+vxJ/MVxMgJwnkHSrSHpRLH2/YDe +gOAvNR2qawdaEhpGFVMbiWTiYz+eo9JZ2yVukGE/g7v/YJTs0I3fKYfslWMzJN/h +5emrxwvR8+fSeBIOHygWhlRudRprj3QryGvLOUeDuCVNu3lQMg8fpyKJeEHnJNQJ +w2IOF6UORiO7FD64LxCRxpjYjsthjMiQu84Ny6pKbHqE2DqxZHOTnDoDzKDVN7Fi +EaLjPUCXMthyZ3iH0UVC013dEhBQ/Qty5tngosDuTVgX44lTL5Cnzbx0x5TCMsJ+ +CP2fVqTDmvXpPaURdNK4JMGzMRJcDjhDnL8FCI9iSutGU6dbhIueM+44/XB2f2YC +wJ8ccH4StrOf7rmaPXlhTNB+GT9l4SkuEBEcMTBENUd7dZxnrrByiD/zMu7FnsOL +hASrucXpJ9Y+zEhg3voFojhHIGhKQ+9jNAlmTJVrSmHfWBT7DssWkXtULhZXTiKn +ctwTOyIxDOZfqd9FZ1eI2panoppOWgDaTRqu0wD6fptiEfo/JzXlXiMqmSRDoO+W +TH/EIdNGdlyVCNiprcjX8IdgDcsadGixTXl7mBjD3jNi8DudA+oGB+Kaxbsy9QMu +l+YetofUArE1dTSsILpc05ytAZ4wgj2w/yHnDtunQQ0TYsrI1edGMsMTKSTgfX34 +sgAthaX1V1m9NOrvRS4WpIBKRYNprIOL7aqalTAlZmtPj31p24+Q2VPHdzUd0S4g +z+mlMZ9KShobHYdqawlPJVIjxl+TpKWrHsZvp8KHtW33irPL4r5jKkylBjrGf7+x +OAWVRVOqtWeCZH1vekrC0ssVzucQJCJc3gz3pDgLPWSxVTYgQRLhSqlky02ksPya ++//D9bY7U6GIWO8ohKwgzpdUV27j9DGRB9LV/Qchpn9R5Ds+QF/bHidT1uzZtW7l +OSXlRW7pPIO2IqHy4QvmsjyOzL8L88hZMkO0bymZTe//haLEUiHCZLrxqMzK6xC4 +d65ZyDAxALbcMNxBcoys+4Udlyjk5Uk/sbyD1QFinz5lxzHGSWAixBlHPgj/OBRr +QcyrWxW5AUQ2h3RqB+oPvuqcP0SpG2Q7trG2XuJVWzcFM9OcAH70IVm+o4MUMNm9 +uwPpKVaTmX55awBqc47BIvrY8mghU+zIsK7JiDTAgvUiDuZOnvietzI37jiy5kjG +x9oBxL4BM8IW+s74d44bfzsqRHCE9ECUq1bGre56/MennrMOXhXjqv4fnYxFPx6C +Tkk2Wct43o2RIMUqK6EC5Txha/N258OaWztZTwm/A/Y6iG8B+YSuQ6DNgLII9t// +9XzcI7R7MPEJ4xO+eTWhdORY9jKcF1Rl2bKZE8O7xQYAG2oFG/ai27EyxrZLcn3H +cksVx/I7W4kxFTATBgkqhkiG9w0BCRUxBgQEAAAAADCCBa8GCSqGSIb3DQEHBqCC +BaAwggWcAgEAMIIFlQYJKoZIhvcNAQcBMCQGCiqGSIb3DQEMAQMwFgQQW9GH8ykz +nqoGw48cJBOdvAICB9CAggVgvg+PTPpFXYUYPyEfau3ufR3E8JXNYWzcasXUyyUI +I2UCTX6Is3SD5Xly2tzn0fxTURdxhsj4miGzRnkI2wfj6P27tYn1LRaJUHozLmsl +pgklasRxeZFRdHgqyJ6xS81luU+3OWiFbYMNXCS9wTkcroY922CRnSXsV9zs9+C8 +sXCMlhlS3gCk/qlcTMhIFj0vVFuaGabgNhR7Jn76U7OQdujczgXgfLHgItVl08oA +PVVqwS3DjJeuIaYDpvOaQzMcjWQtQM9f5maaaR+Wo9h5hGvvoLHnLutzU3eajxK0 +2VygDgM7llVh+BtyTziuyFg/8UBprx+9C7IWJyje+x/7eTiBHyD0tQSP4a8m/awK +Mu90kdvvVVOgfEJhobkQWtTkWOnSPj7xOJWcBT7IHn73WYJs8p+1MHFgcJLT30ow +yS4xzTWXt63AlPGbAcqlVXYORJLcZ/eZnj3oJk2p/hBkj09ghu4H43GsSNThsnR+ +sOl8DKluJIFul+tNfiUKHSMtDrmTxq2wHOMaX+4qcU1uUpnixye6HnlIfJWbo8E5 +o1voQ23xvHDRFmt/Mq5gHinVwSwa2V3NXkp7Dzl8XTnbwhp1mHtH8ssucnOjsC1g +N+HybMd6kMcO8U+qDWqqTac6tA6KK3HqGDuod6+Fdi9EiY5sBgZ/ana8XViscDh5 +Rn9k8qOo22/DXfGraF3FAvPibijLKkqa76VUiUYOgPpGQD0EOIz6fHDlqB+zIvNN +4CHOZAKiOsUmUMJdFLkzLQYaSLb6bKTH7echuoDvXnAMNEuCURcKo/YYQd3Se1gC +j7ulsyo+k3WBMa2LZ6gRJWiD5raDPRVMt+tosvDV/Bbl7xw2Sifkl8g4JJFfhHA8 +tuf19to0sr5SE3nSGMIK4VKyxnhtvFUUN7gRXyJNsupbseGU/zakN9foYOPhx4Op +ve58sEFPWQD4+Tki1zS8KIqulu7fjdZUOBFHX3aF8c36iG77jz5uByxvwPU2v0O1 +UVMU2/wpt8j+ZyEcpS0bdTL7PrhBxJoQAhlFiyxN0t/kCbGUSXZlUfqFHfQLf0Hl +gAbNUJONfRMykYRnE3a6viMMoF86bXEoxBjrNS0BXCt1Z4SWGeZzmnuUT2vBPx91 +l9SZAu+DB+4qP8r9ht1qTWeC9sd+VKrUuW87IPetEsk5BZtw1u+9HrWsGtnHs34s +9i2JFfrdDlPbIcFMLGAgS1Bs1YEe5uxJaAzZtNuSHyN55JODUiwzbiPfjOakmQeV +mUvZwfwP3MANRm+3y8jPoKaDKhoY70lENsR5dyxbkJk0WnSl6XH3nOE9I6M86kqQ +NfuaR21RDHPUHZUxpkhfp1bySW0lA//E0LGTZUjOHw7VXZ3kA0mwzLQ9ySRb9a5h +mTx6qLAI2y9uoxuU6/DODuBPRgz+cb3qHHXDoI3RV8GEDTIZx3Bkbtxv6eK92FQW +3yLeA0uVFwqo79kX00hJuXFWg3r6xwgpJdLJr+Uymhf2g7PmHr9Zip5yWKqb7fP+ +jEvRKE5KD1QFXd/xPxif42VzazF6yX5GF1WrvN1EeUVQZkeXQX+Js3EqoOS/VdJ0 +CI7q5nPGypEnp+1twjqGz5a9qBEqSCjr1RNMRVcDFJC9UxdpPPZT5xFKLhIOe3YD +tunu0saI07dsD05FWrSVvw9bGULbUpd3Ah8Q3WWRasp3mprK38xoUztkxfQ8nWUZ +BeKEIO7DvgrBUw2PBK+s22suRjHwrVOZDJg9/OIeRauusCUqaCoxbIlwdedlhOBO +fhFjJWnvEfHCzN9p8d/bPWw0GmfCkul8mkL8RmL35ZhD487pkpQkl/lKay1V8LFR +YpMwOzAfMAcGBSsOAwIaBBQY9vd9EdUgCQYA+xTPnUa+5tOsrAQUAJL6m/he/l7+ +aGur7E/GqOtPjjECAgfQ"), "PfxPassword"); + + private static X509Certificate2 TestCertificateCerFormat = new X509Certificate2(Convert.FromBase64String(@" +MIIFDTCCAvWgAwIBAgIUMOrXEcRBfYU9Jr6coUXIpSbdLkEwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLTGljZW5zZVRlc3QwHhcNMjQxMDI3MTYyNzM4WhcNMzQx +MDI1MTYyNzM4WjAWMRQwEgYDVQQDDAtMaWNlbnNlVGVzdDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAIiQRt6N+M/tUri0f/9gVr1RnW9KNy4NvIhOtGrw +Z3gbai/gRHdmaL8mNunTqPBYQYm+JOVklmHqJSyI6/cUuy+WgrUU9ewrVZAQUoK7 +DdirP1X0igtOa2gnyQxL7TXzmtQiNSFEbk8SknEteCaMqixpZ3vZHGVSEw0IuFQT +pPSDobmmZxRnOrsfS2TWw1hdiYTrpRCUnjsQ67XjFQmVG1OMuTc4eP611cK+5OfS +g0CFXqjHrQEzbguSbAidlOilkvKwDI1Cb5WOae97JiZEhZoARMuE+XvQdHcFk/u9 +CDpDfO9zK4PeDXFE0u45dsyKEIVFqZ2Ts21Cfa4HwKmtxr//L4jJ4Q2zcOZAqHhO +PbdshhvWKLeypzT22M/02tfy/2uLUfG9SOoejVN0y3vRZEkheTLUypC0AGx5m48D +tnzR2lFZzelIXqIBXJUVBAW45t+KUhHiekQ8GAlK9NDW/9WTNwwPUbhoJyI/Fid9 +B8VJCHC9Qof80+ymqmjoRPZMQTE6/e2A2U3ESUFhbJLKU50e/CQoDOdD+x83zz3U +ayYAjZMdkYEKV/Okj6wKWptnUhzgArnOJxCiuTgbcddoKKSaOzbKA222eR66rIdv +bSAJHyMIwzmwVqyKaDTZdKBsX15vmbQp8pZmSjP0CHk8bQM++lyK8wLTYj321cHl +M2x7AgMBAAGjUzBRMB0GA1UdDgQWBBT9Xt+mJZlanInw5CmJArjMXFbTEDAfBgNV +HSMEGDAWgBT9Xt+mJZlanInw5CmJArjMXFbTEDAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQB2mPFm7hr37eyL9y8OV7hq1sB0n2CjEMoTI36p0NpV +V2rlTnboVifDXtdx/EhMAeiHKcks4Ccn/DvP6sthYsRAGv20+TCjuIkjvt/I4u7C +0udT65oIDUSwcguZ1KYISqE2xBmziN1hJgNKG9hPPeN+mRVX0JxQq3lH4nQSwZD9 +mxX0Ep6eZlKsEvoViYel24KO0WRF+nSx5u7xhi1sA6sS9hQK1NgdQYNqweOFmYuA +G+/PcVXJbVXdleXfsCiQidXHqmlPaNgKVoH/udsBMe+y6V70towJRZsjSAAZjK87 +L4UZFENROmcUMOQE0rCO0KYUBFQPg1UkZFUoOHC3o0Mt0SNC1uJF50MpJDc78Txf +hVraMXRziy9ZSeiqvfffNVv3rVVCs6MrJObmVsm4ZkGVQX7Z7AM7IFVNghy/YOUu +Q9srnDdBXunRt6PYroWD3u4kWFkrJeQMU/TthEm/BfxEkShk59bVVworxYs8d7zL +bp07BTFJ0xhFwv3+jLvwT2WRnmPmWZSySbyeWrU1Pf//4vRlVtnNenxqEUrQW0P7 +p3FMP7lYoJAc0J1uTcJzJT6ncr4+YAkpvMiINYI75fPCVKnGKn71bC70oryPn5ZD +SD9PbavjyDl00o5G6XWPy5x7XfjjLwDN5Lah1t1fCIAFDEleCj1Ml0FaapygrlGw +oA==")); + + [Fact] + public async Task RoundTrip_Works() + { + var cloudSut = CreateSut(options => + { + options.SigningCertificate = TestCertificateWithPrivateKey; + }); + + var license = cloudSut.CreateLicense( + [ + new Claim("myClaim", "hello world!"), + ], TimeSpan.FromMinutes(5)); + + _fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); + + var selfHostSut = CreateSut(options => + { + options.SigningCertificate = TestCertificateCerFormat; + }); + + var claims = await selfHostSut.VerifyLicenseAsync(license); + + Assert.NotEmpty(claims); + Assert.Contains(claims, c => c.Type == "myClaim" && c.Value == "hello world!"); + } + + [Fact] + public void CreateLicense_WithSelfHost_Fails() + { + var selfHostSut = CreateSut(options => + { + options.SigningCertificate = TestCertificateCerFormat; + }); + + var invalidOperation = Assert.Throws( + () => selfHostSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMinutes(5)) + ); + + Assert.Equal( + "Self-hosted services can not create a license, please check 'IsCloud' before calling this method.", + invalidOperation.Message + ); + } + + [Fact] + public async Task RoundTrip_Expired_Fails() + { + + var cloudSut = CreateSut(options => + { + options.SigningCertificate = TestCertificateWithPrivateKey; + }); + + var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMilliseconds(10)); + + await Task.Delay(TimeSpan.FromMilliseconds(100)); + + var selfHostSut = CreateSut(options => + { + options.SigningCertificate = TestCertificateCerFormat; + }); + + var validationException = Assert.ThrowsAsync( + async () => await selfHostSut.VerifyLicenseAsync(license) + ); + } + + // TODO: Test license signed with a different key + + // TODO: Test verifying license with a different key + + private DefaultLicensingService CreateSut(Action configureOptions) + { + var options = new LicensingOptions(); + configureOptions(options); + return new DefaultLicensingService(Options.Create(options), _fakeTimeProvider); + } +} diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs new file mode 100644 index 00000000..4e44bb27 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs @@ -0,0 +1,41 @@ +using Bitwarden.Extensions.Hosting.Licensing; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using NSubstitute; + +namespace Bitwarden.Extensions.Hosting.Tests.Licensing; + +public class PostConfigureLicensingOptionsTests +{ + private readonly InternalLicensingOptions _internalLicensingOptions; + private readonly IHostEnvironment _hostEnvironment; + + private readonly PostConfigureLicensingOptions _sut; + + public PostConfigureLicensingOptionsTests() + { + _internalLicensingOptions = new InternalLicensingOptions(); + _hostEnvironment = Substitute.For(); + + _sut = new PostConfigureLicensingOptions( + Options.Create(_internalLicensingOptions), + NullLogger.Instance, + _hostEnvironment + ); + } + + [Fact] + public void PostConfigure_Works() + { + _hostEnvironment + .ApplicationName + .Returns("Test"); + + var options = new LicensingOptions(); + + _sut.PostConfigure(Options.DefaultName, options); + + Assert.NotNull(options.SigningCertificate); + } +} From 42d5b7b3cf28fe62f8ebb8399beddcc14329e6d0 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:36:24 -0400 Subject: [PATCH 02/13] Add Azure Support & Tests --- .../src/Bitwarden.Extensions.Hosting.csproj | 1 + .../PostConfigureLicensingOptions.cs | 39 +++- .../Bitwarden.Extensions.Hosting.Tests.csproj | 6 + .../Licensing/DefaultLicensingServiceTests.cs | 132 +------------- .../PostConfigureLicensingOptionsTests.cs | 172 +++++++++++++++++- .../tests/Licensing/TestData.cs | 128 +++++++++++++ 6 files changed, 346 insertions(+), 132 deletions(-) create mode 100644 extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj b/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj index 04a35a52..303b48af 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj +++ b/extensions/Bitwarden.Extensions.Hosting/src/Bitwarden.Extensions.Hosting.csproj @@ -22,6 +22,7 @@ + diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs index 534be9f7..ff31bb4f 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Security.Cryptography.X509Certificates; +using Azure.Storage.Blobs; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; @@ -35,6 +36,9 @@ public void PostConfigure(string? name, LicensingOptions options) return; } + // TODO: Apply old config locations to new place + + void DoFinalValidation() { var signingCertificate = options.SigningCertificate; @@ -44,7 +48,14 @@ void DoFinalValidation() throw new InvalidOperationException("No signing certificate could be retrieved."); } - // TODO: Validate thumbprint one last time + var expectedThumbprint = _hostEnvironment.IsDevelopment() + ? _internalLicensingOptions.DevelopmentThumbprint + : _internalLicensingOptions.NonDevelopmentThumbprint; + + if (!string.Equals(expectedThumbprint, signingCertificate.Thumbprint, StringComparison.InvariantCultureIgnoreCase)) + { + throw new InvalidOperationException("The supplied certificate does not contain the expected thumbprint."); + } if (options.ForceSelfHost && !_hostEnvironment.IsDevelopment()) { @@ -96,8 +107,23 @@ private bool TryAzureBlob(LicensingOptions options) return false; } - // TODO: Do azure blob stuff - // TODO: Set SigningCertificate + try + { + var blobServiceClient = new BlobServiceClient(options.AzureBlob.ConnectionString); + var blobContainerClient = blobServiceClient.GetBlobContainerClient(options.AzureBlob.BlobName); + var blobClient = blobContainerClient.GetBlobClient(options.AzureBlob.LicenseName); + using var memoryStream = new MemoryStream(); + blobClient.DownloadTo(memoryStream); + var certificate = new X509Certificate2(memoryStream.ToArray(), options.AzureBlob.CertificatePassword); + + options.SigningCertificate = certificate; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while retrieving signing certificate from azure blob storage."); + throw; // I think we should want this to be as fatal as possible. + } + return true; } @@ -129,7 +155,12 @@ private bool TryGetEmbeddedCert(LicensingOptions options) ? "licensing_dev.cer" : "licensing.cer"; - var resourceName = appAssembly.GetManifestResourceNames().Single(n => n.EndsWith(certName)); + var resourceName = appAssembly.GetManifestResourceNames().SingleOrDefault(n => n.EndsWith(certName)); + + if (resourceName == null) + { + throw new InvalidOperationException($"An embedded certificate ending with the name {certName} could not be found."); + } using var resourceStream = appAssembly.GetManifestResourceStream(resourceName)!; using var memoryStream = new MemoryStream(); diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj b/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj index 2977412d..81f51a4f 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj @@ -19,6 +19,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -31,4 +32,9 @@ + + + + + diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index d3714396..934c7477 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -19,134 +19,14 @@ public DefaultLicensingServiceTests(ITestOutputHelper outputHelper) _fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); } - // Created using: - // openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout test.key -out test.crt -subj "/CN=LicenseTest" -days 3650 - // openssl pkcs12 -export -legacy -out test.pfx -inkey test.key -in test.crt -certfile test.crt - // Password: PfxPassword - // Thumbprint: AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7 - private static X509Certificate2 TestCertificateWithPrivateKey = new X509Certificate2(Convert.FromBase64String(@" -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 -aGur7E/GqOtPjjECAgfQ"), "PfxPassword"); - - private static X509Certificate2 TestCertificateCerFormat = new X509Certificate2(Convert.FromBase64String(@" -MIIFDTCCAvWgAwIBAgIUMOrXEcRBfYU9Jr6coUXIpSbdLkEwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAwwLTGljZW5zZVRlc3QwHhcNMjQxMDI3MTYyNzM4WhcNMzQx -MDI1MTYyNzM4WjAWMRQwEgYDVQQDDAtMaWNlbnNlVGVzdDCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAIiQRt6N+M/tUri0f/9gVr1RnW9KNy4NvIhOtGrw -Z3gbai/gRHdmaL8mNunTqPBYQYm+JOVklmHqJSyI6/cUuy+WgrUU9ewrVZAQUoK7 -DdirP1X0igtOa2gnyQxL7TXzmtQiNSFEbk8SknEteCaMqixpZ3vZHGVSEw0IuFQT -pPSDobmmZxRnOrsfS2TWw1hdiYTrpRCUnjsQ67XjFQmVG1OMuTc4eP611cK+5OfS -g0CFXqjHrQEzbguSbAidlOilkvKwDI1Cb5WOae97JiZEhZoARMuE+XvQdHcFk/u9 -CDpDfO9zK4PeDXFE0u45dsyKEIVFqZ2Ts21Cfa4HwKmtxr//L4jJ4Q2zcOZAqHhO -PbdshhvWKLeypzT22M/02tfy/2uLUfG9SOoejVN0y3vRZEkheTLUypC0AGx5m48D -tnzR2lFZzelIXqIBXJUVBAW45t+KUhHiekQ8GAlK9NDW/9WTNwwPUbhoJyI/Fid9 -B8VJCHC9Qof80+ymqmjoRPZMQTE6/e2A2U3ESUFhbJLKU50e/CQoDOdD+x83zz3U -ayYAjZMdkYEKV/Okj6wKWptnUhzgArnOJxCiuTgbcddoKKSaOzbKA222eR66rIdv -bSAJHyMIwzmwVqyKaDTZdKBsX15vmbQp8pZmSjP0CHk8bQM++lyK8wLTYj321cHl -M2x7AgMBAAGjUzBRMB0GA1UdDgQWBBT9Xt+mJZlanInw5CmJArjMXFbTEDAfBgNV -HSMEGDAWgBT9Xt+mJZlanInw5CmJArjMXFbTEDAPBgNVHRMBAf8EBTADAQH/MA0G -CSqGSIb3DQEBCwUAA4ICAQB2mPFm7hr37eyL9y8OV7hq1sB0n2CjEMoTI36p0NpV -V2rlTnboVifDXtdx/EhMAeiHKcks4Ccn/DvP6sthYsRAGv20+TCjuIkjvt/I4u7C -0udT65oIDUSwcguZ1KYISqE2xBmziN1hJgNKG9hPPeN+mRVX0JxQq3lH4nQSwZD9 -mxX0Ep6eZlKsEvoViYel24KO0WRF+nSx5u7xhi1sA6sS9hQK1NgdQYNqweOFmYuA -G+/PcVXJbVXdleXfsCiQidXHqmlPaNgKVoH/udsBMe+y6V70towJRZsjSAAZjK87 -L4UZFENROmcUMOQE0rCO0KYUBFQPg1UkZFUoOHC3o0Mt0SNC1uJF50MpJDc78Txf -hVraMXRziy9ZSeiqvfffNVv3rVVCs6MrJObmVsm4ZkGVQX7Z7AM7IFVNghy/YOUu -Q9srnDdBXunRt6PYroWD3u4kWFkrJeQMU/TthEm/BfxEkShk59bVVworxYs8d7zL -bp07BTFJ0xhFwv3+jLvwT2WRnmPmWZSySbyeWrU1Pf//4vRlVtnNenxqEUrQW0P7 -p3FMP7lYoJAc0J1uTcJzJT6ncr4+YAkpvMiINYI75fPCVKnGKn71bC70oryPn5ZD -SD9PbavjyDl00o5G6XWPy5x7XfjjLwDN5Lah1t1fCIAFDEleCj1Ml0FaapygrlGw -oA==")); + [Fact] public async Task RoundTrip_Works() { var cloudSut = CreateSut(options => { - options.SigningCertificate = TestCertificateWithPrivateKey; + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateWithPrivateKey, TestData.PfxPassword); }); var license = cloudSut.CreateLicense( @@ -158,7 +38,7 @@ public async Task RoundTrip_Works() var selfHostSut = CreateSut(options => { - options.SigningCertificate = TestCertificateCerFormat; + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); }); var claims = await selfHostSut.VerifyLicenseAsync(license); @@ -172,7 +52,7 @@ public void CreateLicense_WithSelfHost_Fails() { var selfHostSut = CreateSut(options => { - options.SigningCertificate = TestCertificateCerFormat; + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); }); var invalidOperation = Assert.Throws( @@ -191,7 +71,7 @@ public async Task RoundTrip_Expired_Fails() var cloudSut = CreateSut(options => { - options.SigningCertificate = TestCertificateWithPrivateKey; + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateWithPrivateKey, TestData.PfxPassword); }); var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMilliseconds(10)); @@ -200,7 +80,7 @@ public async Task RoundTrip_Expired_Fails() var selfHostSut = CreateSut(options => { - options.SigningCertificate = TestCertificateCerFormat; + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); }); var validationException = Assert.ThrowsAsync( diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs index 4e44bb27..5fbcaac1 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs @@ -1,7 +1,12 @@ +using System.Diagnostics; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Azure.Storage.Blobs; using Bitwarden.Extensions.Hosting.Licensing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using DotNet.Testcontainers.Builders; using NSubstitute; namespace Bitwarden.Extensions.Hosting.Tests.Licensing; @@ -26,16 +31,179 @@ public PostConfigureLicensingOptionsTests() } [Fact] - public void PostConfigure_Works() + public void PostConfigure_NoBlobConfigured_NotInStore_LoadsProductionCert() { + var allowedCertThumbprint = "569A8AD2907FB3A20DE200C04C8B1069E90F20AD"; + + // Update our test cert as the allowed certificate + _internalLicensingOptions.NonDevelopmentThumbprint = allowedCertThumbprint; + _hostEnvironment .ApplicationName - .Returns("Test"); + .Returns("Bitwarden.Extensions.Hosting.Tests"); + + _hostEnvironment + .EnvironmentName + .Returns("Production"); var options = new LicensingOptions(); _sut.PostConfigure(Options.DefaultName, options); Assert.NotNull(options.SigningCertificate); + Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); + } + + [Fact] + public void PostConfigure_NoBlobConfigured_NotInStore_LoadsDevCert() + { + var allowedCertThumbprint = "AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7"; + + // Update our test cert as the allowed certificate + _internalLicensingOptions.DevelopmentThumbprint = allowedCertThumbprint; + + _hostEnvironment + .ApplicationName + .Returns("Bitwarden.Extensions.Hosting.Tests"); + + _hostEnvironment + .EnvironmentName + .Returns("Development"); + + var options = new LicensingOptions(); + + _sut.PostConfigure(Options.DefaultName, options); + + Assert.NotNull(options.SigningCertificate); + Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); + } + + [Fact] + public void PostConfigure_NoBlobConfigured_InStore_Development_LoadsStoreCert() + { + var allowedCertThumbprint = "AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7"; + + // Update our test cert as the allowed certificate + _internalLicensingOptions.DevelopmentThumbprint = allowedCertThumbprint; + + _hostEnvironment + .EnvironmentName + .Returns("Development"); + + var options = new LicensingOptions(); + + UseTempStoreCert( + "Bitwarden.Extensions.Hosting.Tests.Resources.licensing_dev.cer", + allowedCertThumbprint, () => + { + _sut.PostConfigure(Options.DefaultName, options); + }); + + Assert.NotNull(options.SigningCertificate); + Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); + } + + [Fact] + public void PostConfigure_NoBlobConfigured_InStore_Production_LoadsStoreCert() + { + var allowedCertThumbprint = "569A8AD2907FB3A20DE200C04C8B1069E90F20AD"; + + // Update our test cert as the allowed certificate + _internalLicensingOptions.NonDevelopmentThumbprint = allowedCertThumbprint; + + _hostEnvironment + .EnvironmentName + .Returns("Production"); + + var options = new LicensingOptions(); + + UseTempStoreCert( + "Bitwarden.Extensions.Hosting.Tests.Resources.licensing.cer", + allowedCertThumbprint, () => + { + _sut.PostConfigure(Options.DefaultName, options); + }); + + Assert.NotNull(options.SigningCertificate); + Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); + } + + [Fact] + public async Task PostConfigure_InBlob_RetrievesCertFromBlob() + { + await using var test = await PrepareBlobStorageAsync(); + + var allowedCertThumbprint = "AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7"; + + _hostEnvironment + .EnvironmentName + .Returns("Development"); + + _internalLicensingOptions.DevelopmentThumbprint = allowedCertThumbprint; + + var options = new LicensingOptions(); + options.AzureBlob.ConnectionString = "UseDevelopmentStorage=true;"; + options.AzureBlob.CertificatePassword = TestData.PfxPassword; + + _sut.PostConfigure(Options.DefaultName, options); + + Assert.NotNull(options.SigningCertificate); + Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); + } + + private async Task PrepareBlobStorageAsync() + { + var container = new ContainerBuilder() + .WithImage("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .WithPortBinding(10000, 10000) + .Build(); + + await container.StartAsync(); + + // Add certs to blob storage + // TODO: Let test customize these? + var blobServiceClient = new BlobServiceClient("UseDevelopmentStorage=true;"); + var blobContainerClient = blobServiceClient.CreateBlobContainer("certificates").Value; + var blobClient = blobContainerClient.GetBlobClient("licensing.pfx"); + + await blobClient.UploadAsync(new BinaryData(TestData.TestCertificateWithPrivateKey)); + + return container; + } + + private void UseTempStoreCert(string resourceName, string thumbprint, Action test) + { + X509Certificate2? certificate = null; + try + { + var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + certStore.Open(OpenFlags.ReadWrite); + + using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)!; + using var memoryStream = new MemoryStream(); + resourceStream.CopyTo(memoryStream); + certificate = new X509Certificate2(memoryStream.ToArray()); + + // Test code should never have us place a cert that different from the given thumbprint. + Debug.Assert(certificate.Thumbprint == thumbprint); + + certStore.Add(certificate); + + // Close the store before running the test + certStore.Dispose(); + + test(); + } + finally + { + // Was the certificate loaded? + if (certificate != null) + { + // Delete from store via thumbprint + using var deletingCertStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + deletingCertStore.Open(OpenFlags.ReadWrite); + deletingCertStore.Remove(certificate); + } + } } } diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs new file mode 100644 index 00000000..b4d6b1a5 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs @@ -0,0 +1,128 @@ +namespace Bitwarden.Extensions.Hosting.Tests; + +public static class TestData +{ + // Created using: + // openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout test.key -out test.crt -subj "/CN=LicenseTest" -days 3650 + // openssl pkcs12 -export -legacy -out test.pfx -inkey test.key -in test.crt -certfile test.crt + // Password: PfxPassword + // Thumbprint: AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7 + public static byte[] TestCertificateWithPrivateKey = Convert.FromBase64String(@" +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 +aGur7E/GqOtPjjECAgfQ"); + + public static string PfxPassword = "PfxPassword"; + + public static byte[] TestCertificateCerFormat = Convert.FromBase64String(@" +MIIFDTCCAvWgAwIBAgIUMOrXEcRBfYU9Jr6coUXIpSbdLkEwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLTGljZW5zZVRlc3QwHhcNMjQxMDI3MTYyNzM4WhcNMzQx +MDI1MTYyNzM4WjAWMRQwEgYDVQQDDAtMaWNlbnNlVGVzdDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAIiQRt6N+M/tUri0f/9gVr1RnW9KNy4NvIhOtGrw +Z3gbai/gRHdmaL8mNunTqPBYQYm+JOVklmHqJSyI6/cUuy+WgrUU9ewrVZAQUoK7 +DdirP1X0igtOa2gnyQxL7TXzmtQiNSFEbk8SknEteCaMqixpZ3vZHGVSEw0IuFQT +pPSDobmmZxRnOrsfS2TWw1hdiYTrpRCUnjsQ67XjFQmVG1OMuTc4eP611cK+5OfS +g0CFXqjHrQEzbguSbAidlOilkvKwDI1Cb5WOae97JiZEhZoARMuE+XvQdHcFk/u9 +CDpDfO9zK4PeDXFE0u45dsyKEIVFqZ2Ts21Cfa4HwKmtxr//L4jJ4Q2zcOZAqHhO +PbdshhvWKLeypzT22M/02tfy/2uLUfG9SOoejVN0y3vRZEkheTLUypC0AGx5m48D +tnzR2lFZzelIXqIBXJUVBAW45t+KUhHiekQ8GAlK9NDW/9WTNwwPUbhoJyI/Fid9 +B8VJCHC9Qof80+ymqmjoRPZMQTE6/e2A2U3ESUFhbJLKU50e/CQoDOdD+x83zz3U +ayYAjZMdkYEKV/Okj6wKWptnUhzgArnOJxCiuTgbcddoKKSaOzbKA222eR66rIdv +bSAJHyMIwzmwVqyKaDTZdKBsX15vmbQp8pZmSjP0CHk8bQM++lyK8wLTYj321cHl +M2x7AgMBAAGjUzBRMB0GA1UdDgQWBBT9Xt+mJZlanInw5CmJArjMXFbTEDAfBgNV +HSMEGDAWgBT9Xt+mJZlanInw5CmJArjMXFbTEDAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQB2mPFm7hr37eyL9y8OV7hq1sB0n2CjEMoTI36p0NpV +V2rlTnboVifDXtdx/EhMAeiHKcks4Ccn/DvP6sthYsRAGv20+TCjuIkjvt/I4u7C +0udT65oIDUSwcguZ1KYISqE2xBmziN1hJgNKG9hPPeN+mRVX0JxQq3lH4nQSwZD9 +mxX0Ep6eZlKsEvoViYel24KO0WRF+nSx5u7xhi1sA6sS9hQK1NgdQYNqweOFmYuA +G+/PcVXJbVXdleXfsCiQidXHqmlPaNgKVoH/udsBMe+y6V70towJRZsjSAAZjK87 +L4UZFENROmcUMOQE0rCO0KYUBFQPg1UkZFUoOHC3o0Mt0SNC1uJF50MpJDc78Txf +hVraMXRziy9ZSeiqvfffNVv3rVVCs6MrJObmVsm4ZkGVQX7Z7AM7IFVNghy/YOUu +Q9srnDdBXunRt6PYroWD3u4kWFkrJeQMU/TthEm/BfxEkShk59bVVworxYs8d7zL +bp07BTFJ0xhFwv3+jLvwT2WRnmPmWZSySbyeWrU1Pf//4vRlVtnNenxqEUrQW0P7 +p3FMP7lYoJAc0J1uTcJzJT6ncr4+YAkpvMiINYI75fPCVKnGKn71bC70oryPn5ZD +SD9PbavjyDl00o5G6XWPy5x7XfjjLwDN5Lah1t1fCIAFDEleCj1Ml0FaapygrlGw +oA=="); +} From 54481393b3c83de683877800402d45543852b890 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:36:38 -0400 Subject: [PATCH 03/13] Embed Certs --- .../tests/Resources/licensing.cer | Bin 0 -> 1305 bytes .../tests/Resources/licensing_dev.cer | Bin 0 -> 1297 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 extensions/Bitwarden.Extensions.Hosting/tests/Resources/licensing.cer create mode 100644 extensions/Bitwarden.Extensions.Hosting/tests/Resources/licensing_dev.cer diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Resources/licensing.cer b/extensions/Bitwarden.Extensions.Hosting/tests/Resources/licensing.cer new file mode 100644 index 0000000000000000000000000000000000000000..857e3dc87024b975695c523cc4eb43d0a1e57319 GIT binary patch literal 1305 zcmXqLVih%LV*0y)nTe5!NhGMxi08n8zO9je@7~|~E=wk(p|;n6myJ`a&7UO_aHhrbd#~Uk)igr46E^7|gG?(1y!Pfuz&X&G!p&OYTt`{25lgyK1kB-!8fbTrh8Vb4JqfU zJ6?AA{-lt7aXcL6%egJHKDSO*Y~c8G;PtOi&(l1LzMnd7|MS>bw9)B)??YeNVvQeD z0+!ct&QxC5be-8&t!VkgeGg{I@9wpdKO4HzdYg0gvzZST1e)i^9xv*(^iJ9R{KB&x z--LHb{p1nt+s1Z&ect8z<%(}ZLigpSJz6j&LMm6cbYWBSA=`gq&#yB}&Rv!u=Ow&b zc;9<-y@?9m*Z-I}=_ph?rtIMOcW}j?0y8k z!3Kc_vcS|Q%f}+dBH|&LJR>^8<*#wt#%M{;xV6(=|JVx75Ub{US2y(u2BV$rAmtcVG1}pWJ`aUh)0wbs{^K zmM;9Fc|75?$jOp7Ph&2=&+FUxvLJa4t93>7M-@*o1JhHF?zM%Uy|SS6rr8IR&E2>A zT3mB6F}?Ndxz?%wZzS4+1#XoBU6U+|0yh zb%%?!GX8%%eSXK06~Tu*H4EouB|A;} z$XAqTCAEOBf9eqf=jk?mD-H(KG+ww^C9p1%!Q@!I_OB~`DYF(uN&PV7KhBUI>&6=r zHvPn0w|k4`O=dc;uO+faW41HjP44uBPa8Rx3Nv~?m14T`?Z?fnPv*F=?D(g6ySaLY zT7N>o3ig?%$z4Fh`%ljUNpS!Rqd1ckw(?_mdH@?GgWYtRT o%G-Rt0Y5g$AGoS;`jPy_d0KK|_y6z-w(ojw8FsF9`rS$M0m(l#4*&oF literal 0 HcmV?d00001 diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Resources/licensing_dev.cer b/extensions/Bitwarden.Extensions.Hosting/tests/Resources/licensing_dev.cer new file mode 100644 index 0000000000000000000000000000000000000000..8a65d88a6f3ba40d4d23f77f0aa8bb9839e6eea1 GIT binary patch literal 1297 zcmXqLV&yexV*0v(nTe5!NyOmQb-^Q!wXL>l`{pckJ+V~nuAZX-FB_*;n@8JsUPeZ4 zRt5txLlFZZHs(+kW*%;z%;eO(;?$7T;t~Tnab6=6LjxmoLo*|DV~Z$pUSkl~6w1Y^ zuZc+s*$PHh2IeLveg>ecxR{!l7#TVyxZUghasF-4jxF{76T+Wt{nEwlmz|-0lE?e4>E~HjluQ*}^8AG+73x-~^{mp#Os~EvlNuz<%dsOwc*&RM zg*%s}i=1zvA`EXp}mI=E-2xkbgltyd52d-D8JvqNj# zisNe;jq|uC<#5cM@?zon_gXE`e6pm2)R=&ULC?$9`bt+GG3w>vx=d$h*1VnZt?-Kilm&ZPM2? zwr^T)^6kd?FSo9L`k&n$_;IhtE4kj_lGD`}Q#=(bjjo)Uu!SL~a&|xSwwjB#0wd49 z^oU!;7&BFrg>}cX`&~hTkE&d3BsjgkT)6iC>SS{s{=gj>>Pq%v>b2}gJvj>YI<^0~ z{ASszj2AB7d>jp}{=RLv>3hV}F)?S-so=SCe^fMho;&}RH$QKCC0mW5ce3onMy~MB zOZwMvMa@nRl6kDE4!q#+U znA|K`kP{!5KXZ%br)g_NBL*`Yxup{wmq{?A^yU-5loCLf=HVuH2AyY|c9PxOHZCgY^6Jg`p>NL+?&~ zdVhn)gwCtSS7rKV+~5jp{J--yqv88aFXO&!>)~{rt?a=d*|XkSzg1GiInXLy#NY|b zr44--mWi;0@HdC5q=af%6l`DYtb0+}>DnXL=gyic=GGr=;#;F`8I}}x>qmONShe^2 zebeagYeStjFVl@ zC(cWL7CB{;=bm{{TTN~M|9|u)HSFfus+uf8uM5%6zn2&K*zb&3FhSb~aaeOa`pfBrOQ4}1UI)sIhDmR#y{ds*6ldQNrh kkH`89XP<0ac Date: Fri, 1 Nov 2024 09:23:35 -0400 Subject: [PATCH 04/13] Add AzureBlob Tests --- .../src/Licensing/LicensingOptions.cs | 1 + .../Licensing/DefaultLicensingServiceTests.cs | 2 - .../PostConfigureLicensingOptionsTests.cs | 49 +++++- .../tests/XUnitLoggerProvider.cs | 140 ++++++++++++++++++ 4 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs index e50ac572..3b74f032 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs @@ -39,6 +39,7 @@ public sealed class AzureBlobLicensingOptions /// public string? CertificatePassword { get; set; } + // TODO: Do we actually need to allow these to be customized? /// /// The name of the blob the certificate is stored in, defaults to certificates. /// diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index 934c7477..400a111f 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -19,8 +19,6 @@ public DefaultLicensingServiceTests(ITestOutputHelper outputHelper) _fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); } - - [Fact] public async Task RoundTrip_Works() { diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs index 5fbcaac1..45fb144c 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs @@ -4,10 +4,12 @@ using Azure.Storage.Blobs; using Bitwarden.Extensions.Hosting.Licensing; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using DotNet.Testcontainers.Builders; using NSubstitute; +using Xunit.Abstractions; namespace Bitwarden.Extensions.Hosting.Tests.Licensing; @@ -15,17 +17,23 @@ public class PostConfigureLicensingOptionsTests { private readonly InternalLicensingOptions _internalLicensingOptions; private readonly IHostEnvironment _hostEnvironment; + private readonly ILoggerFactory _loggerFactory; private readonly PostConfigureLicensingOptions _sut; - public PostConfigureLicensingOptionsTests() + public PostConfigureLicensingOptionsTests(ITestOutputHelper testOutputHelper) { + _loggerFactory = LoggerFactory.Create(builder => + { + builder.AddXunit(testOutputHelper); + }); + _internalLicensingOptions = new InternalLicensingOptions(); _hostEnvironment = Substitute.For(); _sut = new PostConfigureLicensingOptions( Options.Create(_internalLicensingOptions), - NullLogger.Instance, + _loggerFactory.CreateLogger(), _hostEnvironment ); } @@ -151,20 +159,47 @@ public async Task PostConfigure_InBlob_RetrievesCertFromBlob() Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); } - private async Task PrepareBlobStorageAsync() + [Fact] + public async Task PostConfigure_InBlob_CustomOptions_RetrievesCertFromBlob() + { + await using var test = await PrepareBlobStorageAsync("custom", "myLicense.pfx"); + + var allowedCertThumbprint = "AC6C1CDD9050FC943A4A67DAA181C85CF89AE9C7"; + + _hostEnvironment + .EnvironmentName + .Returns("Development"); + + _internalLicensingOptions.DevelopmentThumbprint = allowedCertThumbprint; + + var options = new LicensingOptions(); + options.AzureBlob.ConnectionString = "UseDevelopmentStorage=true;"; + options.AzureBlob.CertificatePassword = TestData.PfxPassword; + options.AzureBlob.BlobName = "custom"; + options.AzureBlob.LicenseName = "myLicense.pfx"; + + _sut.PostConfigure(Options.DefaultName, options); + + Assert.NotNull(options.SigningCertificate); + Assert.Equal(allowedCertThumbprint, options.SigningCertificate.Thumbprint); + } + + private async Task PrepareBlobStorageAsync( + string containerName = "certificates", + string licenseName = "licensing.pfx") { var container = new ContainerBuilder() .WithImage("mcr.microsoft.com/azure-storage/azurite:3.33.0") - .WithPortBinding(10000, 10000) + .WithPortBinding(10000, 10000) // Default port for blob storage + .WithLogger(_loggerFactory.CreateLogger("Testcontainer")) .Build(); await container.StartAsync(); // Add certs to blob storage - // TODO: Let test customize these? var blobServiceClient = new BlobServiceClient("UseDevelopmentStorage=true;"); - var blobContainerClient = blobServiceClient.CreateBlobContainer("certificates").Value; - var blobClient = blobContainerClient.GetBlobClient("licensing.pfx"); + var blobContainerClient = blobServiceClient.CreateBlobContainer(containerName).Value; + var blobClient = blobContainerClient.GetBlobClient(licenseName); await blobClient.UploadAsync(new BinaryData(TestData.TestCertificateWithPrivateKey)); diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs b/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs new file mode 100644 index 00000000..fa61d128 --- /dev/null +++ b/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs @@ -0,0 +1,140 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + + +namespace Bitwarden.Extensions.Hosting.Tests; + +public static class XunitLoggerFactoryExtensions +{ + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output)); + return builder; + } +} + +// Ref: https://github.com/dotnet/aspnetcore/blob/main/src/Testing/src/Logging/XunitLoggerProvider.cs +// Perma-link: https://github.com/dotnet/aspnetcore/blob/7f62ae6455a90b1026bd1522f4af3e6de64ee2a9/src/Testing/src/Logging/XunitLoggerProvider.cs +public class XunitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } +} + +public class XunitLogger : ILogger +{ + private static readonly string[] NewLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private readonly DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. + var messageBuilder = new StringBuilder(); + + var timestamp = _logStart.HasValue ? + $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3", CultureInfo.InvariantCulture)}s" : + DateTimeOffset.UtcNow.ToString("s", CultureInfo.InvariantCulture); + + var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: "; + var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty); + + var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); + foreach (var line in lines.Skip(1)) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + + if (exception != null) + { + lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + additionalLinePrefix = "| "; + foreach (var line in lines) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + } + + // Remove the last line-break, because ITestOutputHelper only has WriteLine. + var message = messageBuilder.ToString(); + if (message.EndsWith(Environment.NewLine, StringComparison.Ordinal)) + { + message = message.Substring(0, message.Length - Environment.NewLine.Length); + } + + try + { + _output.WriteLine(message); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) + where TState : notnull + => new NullScope(); + + private sealed class NullScope : IDisposable + { + public void Dispose() + { + } + } +} From b08a2ab810e21e516dc350047ee537d4ae74c1f6 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:33:56 -0400 Subject: [PATCH 05/13] Change `SelfHostedAttribute` to Use LicensingService --- .../src/Attributes/SelfHostedAttribute.cs | 7 ++++--- .../src/GlobalSettingsBase.cs | 12 ------------ .../src/HostBuilderExtensions.cs | 16 ---------------- .../tests/SelfHostedAttributeTests.cs | 11 ++++++----- 4 files changed, 10 insertions(+), 36 deletions(-) delete mode 100644 extensions/Bitwarden.Extensions.Hosting/src/GlobalSettingsBase.cs diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Attributes/SelfHostedAttribute.cs b/extensions/Bitwarden.Extensions.Hosting/src/Attributes/SelfHostedAttribute.cs index 2fc9f0ec..5b130c9f 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Attributes/SelfHostedAttribute.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Attributes/SelfHostedAttribute.cs @@ -1,4 +1,5 @@ using Bitwarden.Extensions.Hosting.Exceptions; +using Bitwarden.Extensions.Hosting.Licensing; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; @@ -25,12 +26,12 @@ public class SelfHostedAttribute : ActionFilterAttribute /// public override void OnActionExecuting(ActionExecutingContext context) { - var globalSettings = context.HttpContext.RequestServices.GetRequiredService(); - if (SelfHostedOnly && !globalSettings.IsSelfHosted) + var licensingService = context.HttpContext.RequestServices.GetRequiredService(); + if (SelfHostedOnly && licensingService.IsCloud) { throw new BadRequestException("Only allowed when self-hosted."); } - else if (NotSelfHostedOnly && globalSettings.IsSelfHosted) + else if (NotSelfHostedOnly && !licensingService.IsCloud) { throw new BadRequestException("Only allowed when not self-hosted."); } diff --git a/extensions/Bitwarden.Extensions.Hosting/src/GlobalSettingsBase.cs b/extensions/Bitwarden.Extensions.Hosting/src/GlobalSettingsBase.cs deleted file mode 100644 index cc5bf4bc..00000000 --- a/extensions/Bitwarden.Extensions.Hosting/src/GlobalSettingsBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Bitwarden.Extensions.Hosting; - -/// -/// Global settings. -/// -public class GlobalSettingsBase -{ - /// - /// Gets or sets a value indicating whether the application is self-hosted. - /// - public bool IsSelfHosted { get; set; } -} diff --git a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs index 0bad5911..4edd93a2 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs @@ -53,13 +53,6 @@ public static TBuilder UseBitwardenDefaults(this TBuilder builder, Bit ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(bitwardenHostOptions); - builder.Services.AddOptions() - .Configure((options, config) => - { - options.IsSelfHosted = config.GetValue(SelfHostedConfigKey, false); - }); - - if (builder.Configuration.GetValue(SelfHostedConfigKey, false)) { AddSelfHostedConfig(builder.Configuration, builder.Environment); @@ -100,15 +93,6 @@ public static IHostBuilder UseBitwardenDefaults(this IHostBuilder hostBuilder, A /// public static IHostBuilder UseBitwardenDefaults(this IHostBuilder hostBuilder, BitwardenHostOptions bitwardenHostOptions) { - hostBuilder.ConfigureServices((_, services) => - { - services.AddOptions() - .Configure((options, config) => - { - options.IsSelfHosted = config.GetValue("globalSettings:selfHosted", false); - }); - }); - hostBuilder.ConfigureAppConfiguration((context, builder) => { if (context.Configuration.GetValue(SelfHostedConfigKey, false)) diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/SelfHostedAttributeTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/SelfHostedAttributeTests.cs index 54064439..98aa9ad2 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/SelfHostedAttributeTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/SelfHostedAttributeTests.cs @@ -1,5 +1,6 @@ using Bitwarden.Extensions.Hosting.Attributes; using Bitwarden.Extensions.Hosting.Exceptions; +using Bitwarden.Extensions.Hosting.Licensing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -64,12 +65,12 @@ private ActionExecutingContext GetContext(bool selfHosted) { IServiceCollection services = new ServiceCollection(); - var globalSettings = new GlobalSettingsBase - { - IsSelfHosted = selfHosted - }; + var licensingService = Substitute.For(); + licensingService + .IsCloud + .Returns(!selfHosted); - services.AddSingleton(globalSettings); + services.AddSingleton(licensingService); var httpContext = new DefaultHttpContext { From e49220d98711a8c9179cf2ea145cac703ed16b2b Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:34:50 -0400 Subject: [PATCH 06/13] Formatting --- .../src/Licensing/PostConfigureLicensingOptions.cs | 2 +- .../tests/Licensing/DefaultLicensingServiceTests.cs | 3 +-- .../tests/Licensing/PostConfigureLicensingOptionsTests.cs | 3 +-- .../Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs | 2 -- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs index ff31bb4f..9a0905fa 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs @@ -2,8 +2,8 @@ using System.Security.Cryptography.X509Certificates; using Azure.Storage.Blobs; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Bitwarden.Extensions.Hosting.Licensing; diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index 400a111f..d8de7917 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -1,6 +1,5 @@ -using System.Text; -using System.Security.Cryptography.X509Certificates; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using Bitwarden.Extensions.Hosting.Licensing; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs index 45fb144c..4e73204f 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/PostConfigureLicensingOptionsTests.cs @@ -3,11 +3,10 @@ using System.Security.Cryptography.X509Certificates; using Azure.Storage.Blobs; using Bitwarden.Extensions.Hosting.Licensing; +using DotNet.Testcontainers.Builders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using DotNet.Testcontainers.Builders; using NSubstitute; using Xunit.Abstractions; diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs b/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs index fa61d128..fd9636f7 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/XUnitLoggerProvider.cs @@ -1,6 +1,4 @@ -using System; using System.Globalization; -using System.Linq; using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; From 7dd8a28828f55070644648898453aa0d452a465d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:40:27 -0400 Subject: [PATCH 07/13] Make Licensing Optional --- .../src/BitwardenHostOptions.cs | 12 ++++++++++-- .../src/HostBuilderExtensions.cs | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/BitwardenHostOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/BitwardenHostOptions.cs index c844be61..041d5c29 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/BitwardenHostOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/BitwardenHostOptions.cs @@ -6,11 +6,19 @@ namespace Bitwarden.Extensions.Hosting; public class BitwardenHostOptions { /// - /// Gets or sets a value indicating whether to include request logging. + /// Gets or sets a value indicating whether to include request logging, defaults to true. /// public bool IncludeLogging { get; set; } = true; /// - /// Gets or sets a value indicating whether to include metrics. + /// Gets or sets a value indicating whether to include metrics, defaults to true. /// public bool IncludeMetrics { get; set; } = true; + + /// + /// Gets or sets a value indicating if self-hosting capabilities should be added to the service, defaults to false. + /// + /// + /// If this is not turned on, the assumption is made that the service is running in a cloud environment. + /// + public bool IncludeSelfHosting { get; set; } } diff --git a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs index 4edd93a2..e0471fbd 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs @@ -69,7 +69,11 @@ public static TBuilder UseBitwardenDefaults(this TBuilder builder, Bit } AddFeatureFlagServices(builder.Services, builder.Configuration); - AddLicensingServices(builder.Services, builder.Configuration); + + if (bitwardenHostOptions.IncludeSelfHosting) + { + AddLicensingServices(builder.Services, builder.Configuration); + } return builder; } @@ -120,9 +124,16 @@ public static IHostBuilder UseBitwardenDefaults(this IHostBuilder hostBuilder, B hostBuilder.ConfigureServices((context, services) => { AddFeatureFlagServices(services, context.Configuration); - AddLicensingServices(services, context.Configuration); }); + if (bitwardenHostOptions.IncludeSelfHosting) + { + hostBuilder.ConfigureServices((context, services) => + { + AddLicensingServices(services, context.Configuration); + }); + } + return hostBuilder; } From 2cc8f7acab542ecd14321806246faf96b23527ab Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:10:07 -0400 Subject: [PATCH 08/13] Finish DefaultLicensingService Tests --- .../src/Licensing/DefaultLicensingService.cs | 28 +++- .../src/Licensing/ILicensingService.cs | 6 + .../src/Licensing/InvalidLicenseException.cs | 5 + .../Licensing/DefaultLicensingServiceTests.cs | 60 ++++++- .../tests/Licensing/TestData.cs | 148 ++++++++++++++++++ 5 files changed, 241 insertions(+), 6 deletions(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs index 47ce98a0..530315cc 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs @@ -1,6 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; namespace Bitwarden.Extensions.Hosting.Licensing; @@ -9,14 +10,21 @@ internal sealed class DefaultLicensingService : ILicensingService { private readonly LicensingOptions _licensingOptions; private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; - public DefaultLicensingService(IOptions licensingOptions, TimeProvider timeProvider) + public DefaultLicensingService( + IOptions licensingOptions, + TimeProvider timeProvider, + ILogger logger) { ArgumentNullException.ThrowIfNull(licensingOptions); + ArgumentNullException.ThrowIfNull(timeProvider); + ArgumentNullException.ThrowIfNull(logger); // TODO: Do we need to support runtime changes to these settings at all, I don't think we do... _licensingOptions = licensingOptions.Value; _timeProvider = timeProvider; + _logger = logger; // We are cloud if the signing certificate has a private key that can sign licenses and local development // hasn't forced self host. @@ -78,13 +86,29 @@ public async Task> VerifyLicenseAsync(string license) ValidateLifetime = true, ValidateIssuer = false, // TODO: Do we want to have no issuer? ValidateAudience = false, // TODO: Do we want to have no audience? +#if DEBUG + // It's useful to be stricter in tests so that we don't have to wait 5 minutes + ClockSkew = TimeSpan.Zero, +#endif }; var tokenValidationResult = await tokenHandler.ValidateTokenAsync(license, tokenValidateParameters); if (!tokenValidationResult.IsValid) { - throw tokenValidationResult.Exception; + var exception = tokenValidationResult.Exception; + _logger.LogWarning(exception, "The given license is not valid."); + if (exception is SecurityTokenExpiredException securityTokenExpiredException) + { + throw new InvalidLicenseException(InvalidLicenseReason.Expired, null, securityTokenExpiredException); + } + else if (exception is SecurityTokenSignatureKeyNotFoundException securityTokenSignatureKeyNotFoundException) + { + throw new InvalidLicenseException(InvalidLicenseReason.WrongKey, null, securityTokenSignatureKeyNotFoundException); + } + // TODO: Handle other known failures + + throw new InvalidLicenseException(InvalidLicenseReason.Unknown, null, exception); } // Should I even take a ClaimsIdentity and return it here instead of a list of claims? diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs index a2e385b8..302307db 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs @@ -21,6 +21,9 @@ public interface ILicensingService /// /// The claims to include in the license file. /// How long the license should be valid for. + /// + /// The exception that is thrown if this method is called when the service is not running as a cloud service. + /// /// /// A string representation of the license that can be given to people to store with their self hosted instance. /// @@ -30,6 +33,9 @@ public interface ILicensingService /// Verifies that the given license is valid and can have it's contents be trusted. /// /// The license to check. + /// + /// The exception that is thrown when the given license is invalid and data stored in it can not be trusted. + /// /// An enumerable of claims included in the license. Task> VerifyLicenseAsync(string license); } diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs index d48129c8..b4039f41 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/InvalidLicenseException.cs @@ -15,6 +15,11 @@ public enum InvalidLicenseReason /// Expired, + /// + /// The license was signed with a different key than the one that was used to verify it. + /// + WrongKey, + /// /// The license is invalid for an unknown reason, checks logs for additional details. /// diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index d8de7917..25384dfb 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Bitwarden.Extensions.Hosting.Licensing; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; using Xunit.Abstractions; @@ -80,19 +81,70 @@ public async Task RoundTrip_Expired_Fails() options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); }); - var validationException = Assert.ThrowsAsync( + var validationException = await Assert.ThrowsAsync( async () => await selfHostSut.VerifyLicenseAsync(license) ); + + Assert.Equal(InvalidLicenseReason.Expired, validationException.Reason); + } + + [Fact] + public async Task SignedWithAlternateKey_Fails() + { + var cloudSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateAlternate, "PfxPassword"); + }); + + Assert.True(cloudSut.IsCloud); + + var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMinutes(5)); + + var selfHostSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); + }); + + var invalidLicenseException = await Assert.ThrowsAsync( + async () => await selfHostSut.VerifyLicenseAsync(license) + ); + + Assert.Equal(InvalidLicenseReason.WrongKey, invalidLicenseException.Reason); } - // TODO: Test license signed with a different key - // TODO: Test verifying license with a different key + [Fact] + public async Task SignedWithMainKey_VerifyingWithAlternateKey_Fails() + { + var cloudSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateWithPrivateKey, "PfxPassword"); + }); + + Assert.True(cloudSut.IsCloud); + + var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMinutes(5)); + + var selfHostSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormatAlternate); + }); + + var invalidLicenseException = await Assert.ThrowsAsync( + async () => await selfHostSut.VerifyLicenseAsync(license) + ); + + Assert.Equal(InvalidLicenseReason.WrongKey, invalidLicenseException.Reason); + } private DefaultLicensingService CreateSut(Action configureOptions) { var options = new LicensingOptions(); configureOptions(options); - return new DefaultLicensingService(Options.Create(options), _fakeTimeProvider); + return new DefaultLicensingService( + Options.Create(options), + _fakeTimeProvider, + NullLogger.Instance + ); } } diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs index b4d6b1a5..33042866 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/TestData.cs @@ -94,8 +94,124 @@ public static class TestData YpMwOzAfMAcGBSsOAwIaBBQY9vd9EdUgCQYA+xTPnUa+5tOsrAQUAJL6m/he/l7+ aGur7E/GqOtPjjECAgfQ"); + public static byte[] TestCertificateAlternate = Convert.FromBase64String(@" +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"); + public static string PfxPassword = "PfxPassword"; + // Thumbprint: 569A8AD2907FB3A20DE200C04C8B1069E90F20AD public static byte[] TestCertificateCerFormat = Convert.FromBase64String(@" MIIFDTCCAvWgAwIBAgIUMOrXEcRBfYU9Jr6coUXIpSbdLkEwDQYJKoZIhvcNAQEL BQAwFjEUMBIGA1UEAwwLTGljZW5zZVRlc3QwHhcNMjQxMDI3MTYyNzM4WhcNMzQx @@ -125,4 +241,36 @@ public static class TestData p3FMP7lYoJAc0J1uTcJzJT6ncr4+YAkpvMiINYI75fPCVKnGKn71bC70oryPn5ZD SD9PbavjyDl00o5G6XWPy5x7XfjjLwDN5Lah1t1fCIAFDEleCj1Ml0FaapygrlGw oA=="); + + + public static byte[] TestCertificateCerFormatAlternate = Convert.FromBase64String(@" +MIIFFTCCAv2gAwIBAgIUUo4yDMDAjrVZ/d3fte5qHGiAfY0wDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPTGljZW5zZVRlc3RQcm9kMB4XDTI0MTAzMTAwNTMzMVoX +DTM0MTAyOTAwNTMzMVowGjEYMBYGA1UEAwwPTGljZW5zZVRlc3RQcm9kMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxyyF4doNTnaGWYGZN2gSbtSynKyK +Bou9rwDO2tM8FrO7YfIWPEZhldKcoDtwDRhhari8UgKCtRz23iwcUFbzNSdFbwov +ymKqEM0kWRasww+MT0BoYZxj4u+k9mFcVicKF2D7eQ4Tn7cLopkaqEkHJ8KrUB3N +2PFJY35PoHl9zH676KaXqTzx2KlycrlCiaaDVyk3GbFIBo/z3LSO9lWxAkDXcTOe +GW4aBg69sTO4Ti1BIIg/M3XYITOtf9Vbr5vq/BewnM0P1uG4KAgeJGC4BWqyihoS +S7k5ey3IfxKyEa28vAuIBerLpN2HnsMw4UFED8+vy3zKNYIttA4ey92SQ6l/Vib/ +qd8KQo+c2ueMcUBya5PER21QLW962ts8VpLoMCdaYZqIGXpBiPbJeTD8Aig2mZoX +oPY6szW8qK1Udwp+x3bT35JUvl4MCDenCzlq84WTIYAI8sDr+lVJywwhTfKI2/5I +sXKxQt+N4U0dcyj4lFCnfgmZI6GC1wM9JnKnkb7gmh+7jTofzVWpO7ZDe+aZ4KBR +N29dx3KNOUtku+fQ5rj2E7oa+QwVjrYGz69u03+nIe1UVb5tZuKglFgabS11oYJj +wj3+FufXAxmdpmAeShO7E77vNy6RIEvX/DRCLCB7QWS4CP7BqLxwNh4S/C8Z4N+E +IUqqU4EkI5GrxJUCAwEAAaNTMFEwHQYDVR0OBBYEFEgZY5hbaET9M2axWxlJfmEm +yE8XMB8GA1UdIwQYMBaAFEgZY5hbaET9M2axWxlJfmEmyE8XMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAJcpiRO0fF319BSS0uB+xmMYjx3d6o4D +yb/JPyHv664UuKV1ofQpx2DLFMl07OVc0e9ujrHpcGOsBTt4e/EkSRYwNcri3oZV +zdSgddk28DSzi9uOH/Ws8zP8UZ/gWjNMJs06Nh+3VfAE8ZmZBZ9LvWVNuLWNzpsv +D9qqiagr9jWFd54Y+gHMNR041kQ0Ndrm5yrK/+0C4qqtQ3XpJ1qL+mTihDl5SUwh +5zEdFxUqq25SSZvOwDDCch+BoagwPEp7NMExb09d/UCwLwurJLkqnkzeOQAKe+Qb +4zMJf4bgUAmvNP9mWbMCDjrCDKr0WXEVXoFz7FU5pRuNQKbz2U16I4ajPmqW1zwf +EMugXwDZT+w47U4uYhKQJTX8CKkj6ugWqR5sC9U+kiFlja4zbcM3VCk2UyxzoOqV +imj/9svPuMSoU8JJKXGeamNCkvEOcmE6GqAOj5XEMEOXPI6owVB8gdDRehCuWQA0 +xn8r+tROZJqiWhr4MQ/HAGddRg1UVpfInUbeop6TAs8vKhS8KJtDDtkLZ2DysQml +EwFL5RoC1Pb42bXknEQEuP4h24N7uCaPYFCoB1ypoHtzoYVuCeUpaQiNyk9ce6n8 ++dO+4VfO0KJjqXqty8TW1zPcAMSqqSt52w5NUPiyH8DVIMviH9GeKh5W3/wOEYe6 +5zlWzoWX3ZKf"); + } From 2abf460b389c68bd12cc63104381e16efdf18164 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Sat, 2 Nov 2024 11:13:18 -0400 Subject: [PATCH 09/13] Do Require Issuer & Audience --- .../src/HostBuilderExtensions.cs | 10 +++++++ .../src/Licensing/DefaultLicensingService.cs | 16 +++++++---- .../src/Licensing/LicensingOptions.cs | 27 ++++++++++++++++--- .../Bitwarden.Extensions.Hosting.Tests.csproj | 1 - .../Licensing/DefaultLicensingServiceTests.cs | 8 ++++-- 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs index e0471fbd..3daec34a 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/HostBuilderExtensions.cs @@ -243,6 +243,16 @@ private static void AddFeatureFlagServices(IServiceCollection services, IConfigu private static void AddLicensingServices(IServiceCollection services, IConfiguration configuration) { + // Default the product name to the application name if no one else has added it. + services.AddOptions() + .PostConfigure((options, environment) => + { + if (string.IsNullOrEmpty(options.ProductName)) + { + options.ProductName = environment.ApplicationName; + } + }); + services.TryAddEnumerable( ServiceDescriptor.Singleton, PostConfigureLicensingOptions>() ); diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs index 530315cc..fd26755e 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs @@ -11,20 +11,24 @@ internal sealed class DefaultLicensingService : ILicensingService private readonly LicensingOptions _licensingOptions; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; + private readonly InternalLicensingOptions _internalLicensingOptions; public DefaultLicensingService( IOptions licensingOptions, TimeProvider timeProvider, - ILogger logger) + ILogger logger, + IOptions internalLicensingOptions) { ArgumentNullException.ThrowIfNull(licensingOptions); ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(internalLicensingOptions); // TODO: Do we need to support runtime changes to these settings at all, I don't think we do... _licensingOptions = licensingOptions.Value; _timeProvider = timeProvider; _logger = logger; + _internalLicensingOptions = internalLicensingOptions.Value; // We are cloud if the signing certificate has a private key that can sign licenses and local development // hasn't forced self host. @@ -49,8 +53,8 @@ public string CreateLicense(IEnumerable claims, TimeSpan validFor) var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), - // Issuer = "bitwarden.com", // TODO: Is this what we want? - // Audience = "configurable_product_name??", // TODO: Is this what we want? + Issuer = _licensingOptions.CloudHost, + Audience = _internalLicensingOptions.ProductName, SigningCredentials = new SigningCredentials( new X509SecurityKey(_licensingOptions.SigningCertificate), SecurityAlgorithms.RsaSha256), IssuedAt = now, @@ -84,8 +88,10 @@ public async Task> VerifyLicenseAsync(string license) IssuerSigningKey = new X509SecurityKey(_licensingOptions.SigningCertificate), ValidateIssuerSigningKey = true, ValidateLifetime = true, - ValidateIssuer = false, // TODO: Do we want to have no issuer? - ValidateAudience = false, // TODO: Do we want to have no audience? + ValidIssuer = _licensingOptions.CloudHost, + ValidateIssuer = true, + ValidAudience = _internalLicensingOptions.ProductName, + ValidateAudience = true, #if DEBUG // It's useful to be stricter in tests so that we don't have to wait 5 minutes ClockSkew = TimeSpan.Zero, diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs index 3b74f032..6714c6c4 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/LicensingOptions.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Hosting; namespace Bitwarden.Extensions.Hosting.Licensing; @@ -7,6 +8,11 @@ namespace Bitwarden.Extensions.Hosting.Licensing; /// public sealed class LicensingOptions { + /// + /// The base url of the cloud instance. + /// + public string CloudHost { get; set; } = "bitwarden.com"; + /// /// Options for configuring license retrieval from azure blob storage. /// @@ -52,13 +58,28 @@ public sealed class AzureBlobLicensingOptions } /// -/// It's important that these can't be set through configuration, only through code. +/// /// /// -/// We would need to make this public for services to customize this. +/// You should not allow these to be set through configuration, only through code. /// -internal sealed class InternalLicensingOptions +public sealed class InternalLicensingOptions { + /// + /// The name of the product, defaults to the . + /// + /// + /// This should NOT be changed once you have issued licenses, changing this will invalidate any already existing licenses. + /// + public string ProductName { get; set; } = null!; + + /// + /// The thumbprint of the certificate that should be allowed when running in development mode. + /// public string DevelopmentThumbprint { get; set; } = "207E64A231E8AA32AAF68A61037C075EBEBD553F"; + + /// + /// The thumbprint of the certificate that should be allowed when running in non-development mode. + /// public string NonDevelopmentThumbprint { get; set; } = "B34876439FCDA2846505B2EFBBA6C4A951313EBE"; } diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj b/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj index 81f51a4f..7d853579 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Bitwarden.Extensions.Hosting.Tests.csproj @@ -29,7 +29,6 @@ - diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index 25384dfb..ff7397c3 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -137,14 +137,18 @@ public async Task SignedWithMainKey_VerifyingWithAlternateKey_Fails() Assert.Equal(InvalidLicenseReason.WrongKey, invalidLicenseException.Reason); } - private DefaultLicensingService CreateSut(Action configureOptions) + private DefaultLicensingService CreateSut(Action configureOptions, string productName = "test") { var options = new LicensingOptions(); configureOptions(options); return new DefaultLicensingService( Options.Create(options), _fakeTimeProvider, - NullLogger.Instance + NullLogger.Instance, + Options.Create(new InternalLicensingOptions + { + ProductName = productName, + }) ); } } From bdf7a0392326a1004de0dfb843f3fafc21daf5d5 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:24:34 -0400 Subject: [PATCH 10/13] Add Tests For Issuer & Audience Validation --- .../Licensing/DefaultLicensingServiceTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index ff7397c3..b3216830 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -45,6 +45,57 @@ public async Task RoundTrip_Works() Assert.Contains(claims, c => c.Type == "myClaim" && c.Value == "hello world!"); } + [Fact] + public async Task VerifyLicenseAsync_DifferentProduct_Fails() + { + var cloudSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateWithPrivateKey, TestData.PfxPassword); + }, "product1"); + + var license = cloudSut.CreateLicense( + [ + new Claim("myClaim", "hello world!"), + ], TimeSpan.FromMinutes(5)); + + _fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); + + var selfHostSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); + }, "product2"); + + await Assert.ThrowsAsync( + async () => await selfHostSut.VerifyLicenseAsync(license) + ); + } + + [Fact] + public async Task VerifyLicenseAsync_DifferentCloudHost_Fails() + { + var cloudSut = CreateSut(options => + { + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateWithPrivateKey, TestData.PfxPassword); + }); + + var license = cloudSut.CreateLicense( + [ + new Claim("myClaim", "hello world!"), + ], TimeSpan.FromMinutes(5)); + + _fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); + + var selfHostSut = CreateSut(options => + { + options.CloudHost = "bitwarden.eu"; + options.SigningCertificate = new X509Certificate2(TestData.TestCertificateCerFormat); + }); + + await Assert.ThrowsAsync( + async () => await selfHostSut.VerifyLicenseAsync(license) + ); + } + [Fact] public void CreateLicense_WithSelfHost_Fails() { From 5597d20ab6d02f1bb2f9a8a038e9a49fa290e455 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:09:48 -0500 Subject: [PATCH 11/13] PR Feedback --- .../src/Licensing/DefaultLicensingService.cs | 6 ------ .../src/Licensing/PostConfigureLicensingOptions.cs | 1 - 2 files changed, 7 deletions(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs index fd26755e..ff93a8bc 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs @@ -24,7 +24,6 @@ public DefaultLicensingService( ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(internalLicensingOptions); - // TODO: Do we need to support runtime changes to these settings at all, I don't think we do... _licensingOptions = licensingOptions.Value; _timeProvider = timeProvider; _logger = logger; @@ -72,9 +71,6 @@ public string CreateLicense(IEnumerable claims, TimeSpan validFor) public async Task> VerifyLicenseAsync(string license) { ArgumentNullException.ThrowIfNull(license); - // TODO: Should we validate that this is self host? - // It's not technically wrong to be able to do that but we don't do it currently - // so we could disallow it. var tokenHandler = new JwtSecurityTokenHandler(); @@ -113,11 +109,9 @@ public async Task> VerifyLicenseAsync(string license) throw new InvalidLicenseException(InvalidLicenseReason.WrongKey, null, securityTokenSignatureKeyNotFoundException); } // TODO: Handle other known failures - throw new InvalidLicenseException(InvalidLicenseReason.Unknown, null, exception); } - // Should I even take a ClaimsIdentity and return it here instead of a list of claims? return tokenValidationResult.ClaimsIdentity.Claims; } } diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs index 9a0905fa..1e035e9b 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs @@ -64,7 +64,6 @@ void DoFinalValidation() } } - // Try Azure Blob first if (TryAzureBlob(options)) { From 8154ce583f05e56e5bfc93476cf2b5560f363b64 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:56:03 -0500 Subject: [PATCH 12/13] Use `DateTime` For Expiration --- .../src/Licensing/DefaultLicensingService.cs | 10 ++++++---- .../src/Licensing/ILicensingService.cs | 4 ++-- .../Licensing/DefaultLicensingServiceTests.cs | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs index ff93a8bc..55165b27 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/DefaultLicensingService.cs @@ -36,17 +36,19 @@ public DefaultLicensingService( public bool IsCloud { get; } - public string CreateLicense(IEnumerable claims, TimeSpan validFor) + public string CreateLicense(IEnumerable claims, DateTime expirationDate) { ArgumentNullException.ThrowIfNull(claims); - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(validFor, TimeSpan.Zero); + var now = _timeProvider.GetUtcNow().UtcDateTime; + + // Expiration date must be in the future + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(expirationDate, now); if (!IsCloud) { throw new InvalidOperationException("Self-hosted services can not create a license, please check 'IsCloud' before calling this method."); } - var now = _timeProvider.GetUtcNow().UtcDateTime; var tokenDescriptor = new SecurityTokenDescriptor @@ -58,7 +60,7 @@ public string CreateLicense(IEnumerable claims, TimeSpan validFor) new X509SecurityKey(_licensingOptions.SigningCertificate), SecurityAlgorithms.RsaSha256), IssuedAt = now, NotBefore = now, - Expires = now.Add(validFor), + Expires = expirationDate, }; var tokenHandler = new JwtSecurityTokenHandler(); diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs index 302307db..fd19bd8c 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/ILicensingService.cs @@ -20,14 +20,14 @@ public interface ILicensingService /// This method can only be called when returns . /// /// The claims to include in the license file. - /// How long the license should be valid for. + /// The date the generated license should expire. /// /// The exception that is thrown if this method is called when the service is not running as a cloud service. /// /// /// A string representation of the license that can be given to people to store with their self hosted instance. /// - string CreateLicense(IEnumerable claims, TimeSpan validFor); + string CreateLicense(IEnumerable claims, DateTime expirationDate); /// /// Verifies that the given license is valid and can have it's contents be trusted. diff --git a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs index b3216830..71272fa9 100644 --- a/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs +++ b/extensions/Bitwarden.Extensions.Hosting/tests/Licensing/DefaultLicensingServiceTests.cs @@ -30,7 +30,7 @@ public async Task RoundTrip_Works() var license = cloudSut.CreateLicense( [ new Claim("myClaim", "hello world!"), - ], TimeSpan.FromMinutes(5)); + ], Expiration(TimeSpan.FromMinutes(5))); _fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); @@ -56,7 +56,7 @@ public async Task VerifyLicenseAsync_DifferentProduct_Fails() var license = cloudSut.CreateLicense( [ new Claim("myClaim", "hello world!"), - ], TimeSpan.FromMinutes(5)); + ], Expiration(TimeSpan.FromMinutes(5))); _fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); @@ -81,7 +81,7 @@ public async Task VerifyLicenseAsync_DifferentCloudHost_Fails() var license = cloudSut.CreateLicense( [ new Claim("myClaim", "hello world!"), - ], TimeSpan.FromMinutes(5)); + ], Expiration(TimeSpan.FromMinutes(5))); _fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); @@ -105,7 +105,7 @@ public void CreateLicense_WithSelfHost_Fails() }); var invalidOperation = Assert.Throws( - () => selfHostSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMinutes(5)) + () => selfHostSut.CreateLicense(Enumerable.Empty(), Expiration(TimeSpan.FromMinutes(5))) ); Assert.Equal( @@ -123,7 +123,7 @@ public async Task RoundTrip_Expired_Fails() options.SigningCertificate = new X509Certificate2(TestData.TestCertificateWithPrivateKey, TestData.PfxPassword); }); - var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMilliseconds(10)); + var license = cloudSut.CreateLicense(Enumerable.Empty(), Expiration(TimeSpan.FromMilliseconds(10))); await Task.Delay(TimeSpan.FromMilliseconds(100)); @@ -149,7 +149,7 @@ public async Task SignedWithAlternateKey_Fails() Assert.True(cloudSut.IsCloud); - var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMinutes(5)); + var license = cloudSut.CreateLicense(Enumerable.Empty(), Expiration(TimeSpan.FromMinutes(5))); var selfHostSut = CreateSut(options => { @@ -174,7 +174,7 @@ public async Task SignedWithMainKey_VerifyingWithAlternateKey_Fails() Assert.True(cloudSut.IsCloud); - var license = cloudSut.CreateLicense(Enumerable.Empty(), TimeSpan.FromMinutes(5)); + var license = cloudSut.CreateLicense(Enumerable.Empty(), Expiration(TimeSpan.FromMinutes(5))); var selfHostSut = CreateSut(options => { @@ -188,6 +188,11 @@ public async Task SignedWithMainKey_VerifyingWithAlternateKey_Fails() Assert.Equal(InvalidLicenseReason.WrongKey, invalidLicenseException.Reason); } + private DateTime Expiration(TimeSpan timeSpan) + { + return _fakeTimeProvider.GetUtcNow().UtcDateTime.Add(timeSpan); + } + private DefaultLicensingService CreateSut(Action configureOptions, string productName = "test") { var options = new LicensingOptions(); From 04c405f6b01efe7f59d7512636b68e759ae40d28 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 7 Nov 2024 02:03:50 -0500 Subject: [PATCH 13/13] Remove Extra Space --- .../src/Licensing/PostConfigureLicensingOptions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs index 1e035e9b..0f0d2c13 100644 --- a/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs +++ b/extensions/Bitwarden.Extensions.Hosting/src/Licensing/PostConfigureLicensingOptions.cs @@ -38,7 +38,6 @@ public void PostConfigure(string? name, LicensingOptions options) // TODO: Apply old config locations to new place - void DoFinalValidation() { var signingCertificate = options.SigningCertificate;