Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 51 additions & 16 deletions src/Core/Billing/Services/Implementations/LicensingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ namespace Bit.Core.Billing.Services;

public class LicensingService : ILicensingService
{
private readonly X509Certificate2 _certificate;
private const string _productionCertThumbprint = "β€ŽB34876439FCDA2846505B2EFBBA6C4A951313EBE";
private const string _developmentCertThumbprint = "207E64A231E8AA32AAF68A61037C075EBEBD553F";
private readonly X509Certificate2 _creationCertificate;
private readonly HashSet<X509Certificate2> _verificationCertificates;
private readonly IGlobalSettings _globalSettings;
private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository;
Expand Down Expand Up @@ -63,31 +66,63 @@ public LicensingService(
_userLicenseClaimsFactory = userLicenseClaimsFactory;
_pushNotificationService = pushNotificationService;

var certThumbprint = environment.IsDevelopment() ?
"207E64A231E8AA32AAF68A61037C075EBEBD553F" :
"β€ŽB34876439FCDA2846505B2EFBBA6C4A951313EBE";

// Load license creation cert
var creationCertThumbprint = environment.IsDevelopment() ? _developmentCertThumbprint : _productionCertThumbprint;
_verificationCertificates = new HashSet<X509Certificate2>();
if (_globalSettings.SelfHosted)
{
_certificate = CoreHelpers.GetEmbeddedCertificateAsync(environment.IsDevelopment() ? "licensing_dev.cer" : "licensing.cer", null)
.GetAwaiter().GetResult();
X509Certificate2 devCert = null;
X509Certificate2 prodCert = CoreHelpers.GetEmbeddedCertificateAsync("licensing.cer", null).GetAwaiter().GetResult();

if (environment.IsDevelopment())
{
devCert = CoreHelpers.GetEmbeddedCertificateAsync("licensing_dev.cer", null).GetAwaiter().GetResult();
_creationCertificate = devCert;
// All self host envs accept prod cert. Creation cert added below to handle dev self-hosts
_verificationCertificates.Add(prodCert);
}
else
{
_creationCertificate = prodCert;
}

// non-production environments can use dev cert-generated licenses
if (!environment.IsProduction())
{
devCert ??= CoreHelpers.GetEmbeddedCertificateAsync("licensing_dev.cer", null).GetAwaiter().GetResult();
_verificationCertificates.Add(devCert);
}
Comment thread
kdenney marked this conversation as resolved.
}
else if (CoreHelpers.SettingHasValue(_globalSettings.Storage?.ConnectionString) &&
CoreHelpers.SettingHasValue(_globalSettings.LicenseCertificatePassword))
{
_certificate = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
_creationCertificate = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
"licensing.pfx", _globalSettings.LicenseCertificatePassword)
.GetAwaiter().GetResult();
}
else
{
_certificate = CoreHelpers.GetCertificate(certThumbprint);
_creationCertificate = CoreHelpers.GetCertificate(creationCertThumbprint);
}
// Creation cert can always be used to verify
_verificationCertificates.Add(_creationCertificate);

if (_certificate == null || !_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(certThumbprint),
if (_creationCertificate == null || !_creationCertificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(creationCertThumbprint),
StringComparison.InvariantCultureIgnoreCase))
{
throw new Exception("Invalid licensing certificate.");
}
var allowedThumbprints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
CoreHelpers.CleanCertificateThumbprint(_productionCertThumbprint),
CoreHelpers.CleanCertificateThumbprint(_developmentCertThumbprint)
};
if (_verificationCertificates is null || _verificationCertificates.Count == 0
|| _verificationCertificates.Any(c => !allowedThumbprints.Contains(c.Thumbprint)))
{
throw new Exception("Invalid license verifying certificate.");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clean, nice! πŸ˜„


if (_globalSettings.SelfHosted && !CoreHelpers.SettingHasValue(_globalSettings.LicenseDirectory))
{
Expand Down Expand Up @@ -132,7 +167,7 @@ public async Task ValidateOrganizationsAsync()
continue;
}

if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
if (string.IsNullOrWhiteSpace(license.Token) && !_verificationCertificates.Any(c => license.VerifySignature(c)))
{
await DisableOrganizationAsync(org, license, "Invalid signature.");
continue;
Expand Down Expand Up @@ -231,7 +266,7 @@ private async Task<bool> ProcessUserValidationAsync(User user)
return false;
}

if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
if (string.IsNullOrWhiteSpace(license.Token) && !_verificationCertificates.Any(c => license.VerifySignature(c)))
{
await DisablePremiumAsync(user, license, "Invalid signature.");
return false;
Expand Down Expand Up @@ -271,7 +306,7 @@ public bool VerifyLicense(ILicense license)
{
if (string.IsNullOrWhiteSpace(license.Token))
{
return license.VerifySignature(_certificate);
return _verificationCertificates.Any((c) => license.VerifySignature(c));
}

try
Expand All @@ -288,12 +323,12 @@ public bool VerifyLicense(ILicense license)

public byte[] SignLicense(ILicense license)
{
if (_globalSettings.SelfHosted || !_certificate.HasPrivateKey)
if (_globalSettings.SelfHosted || !_creationCertificate.HasPrivateKey)
{
throw new InvalidOperationException("Cannot sign licenses.");
}

return license.Sign(_certificate);
return license.Sign(_creationCertificate);
}

private UserLicense ReadUserLicense(User user)
Expand Down Expand Up @@ -341,7 +376,7 @@ public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new X509SecurityKey(_certificate),
IssuerSigningKeys = _verificationCertificates.Select(c => new X509SecurityKey(c)),
ValidateIssuer = true,
ValidIssuer = "bitwarden",
ValidateAudience = true,
Expand Down Expand Up @@ -393,7 +428,7 @@ private string GenerateToken(List<Claim> claims, string audience)
claims.Add(new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()));
}

var securityKey = new RsaSecurityKey(_certificate.GetRSAPrivateKey());
var securityKey = new RsaSecurityKey(_creationCertificate.GetRSAPrivateKey());
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expand Down
4 changes: 3 additions & 1 deletion src/Core/Services/Play/Implementations/PlayIdService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class PlayIdService(IHostEnvironment hostEnvironment) : IPlayIdService
public bool InPlay(out string playId)
{
playId = PlayId ?? string.Empty;
return !string.IsNullOrEmpty(PlayId) && hostEnvironment.IsDevelopment();
var hasPlayId = !string.IsNullOrEmpty(playId);
var isNotProd = !hostEnvironment.IsProduction();
return hasPlayId && isNotProd;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public string? PlayId

public bool InPlay(out string playId)
{
if (hostEnvironment.IsDevelopment())
if (!hostEnvironment.IsProduction())
Comment thread
MGibson1 marked this conversation as resolved.
{
return Current.InPlay(out playId);
}
Expand Down
14 changes: 7 additions & 7 deletions src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,10 +478,6 @@ public static void AddCustomDataProtectionServices(
this IServiceCollection services, IWebHostEnvironment env, GlobalSettings globalSettings)
{
var builder = services.AddDataProtection().SetApplicationName("Bitwarden");
if (env.IsDevelopment())
{
return;
}

if (globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.DataProtection.Directory))
{
Expand All @@ -502,9 +498,13 @@ public static void AddCustomDataProtectionServices(
"dataprotection.pfx", globalSettings.DataProtection.CertificatePassword)
.GetAwaiter().GetResult();
}
builder
.PersistKeysToAzureBlobStorage(globalSettings.Storage.ConnectionString, "aspnet-dataprotection", "keys.xml")
.ProtectKeysWithCertificate(dataProtectionCert);

if (!env.IsDevelopment())
{
builder
.PersistKeysToAzureBlobStorage(globalSettings.Storage.ConnectionString, "aspnet-dataprotection", "keys.xml")
.ProtectKeysWithCertificate(dataProtectionCert);
}
}
}

Expand Down
Loading