@@ -31,7 +31,10 @@ namespace Bit.Core.Billing.Services;
3131
3232public class LicensingService : ILicensingService
3333{
34- private readonly X509Certificate2 _certificate ;
34+ private const string _productionCertThumbprint = "B34876439FCDA2846505B2EFBBA6C4A951313EBE" ;
35+ private const string _developmentCertThumbprint = "207E64A231E8AA32AAF68A61037C075EBEBD553F" ;
36+ private readonly X509Certificate2 _creationCertificate ;
37+ private readonly HashSet < X509Certificate2 > _verificationCertificates ;
3538 private readonly IGlobalSettings _globalSettings ;
3639 private readonly IUserRepository _userRepository ;
3740 private readonly IOrganizationRepository _organizationRepository ;
@@ -63,31 +66,63 @@ public LicensingService(
6366 _userLicenseClaimsFactory = userLicenseClaimsFactory ;
6467 _pushNotificationService = pushNotificationService ;
6568
66- var certThumbprint = environment . IsDevelopment ( ) ?
67- "207E64A231E8AA32AAF68A61037C075EBEBD553F" :
68- "B34876439FCDA2846505B2EFBBA6C4A951313EBE" ;
69+
70+ // Load license creation cert
71+ var creationCertThumbprint = environment . IsDevelopment ( ) ? _developmentCertThumbprint : _productionCertThumbprint ;
72+ _verificationCertificates = new HashSet < X509Certificate2 > ( ) ;
6973 if ( _globalSettings . SelfHosted )
7074 {
71- _certificate = CoreHelpers . GetEmbeddedCertificateAsync ( environment . IsDevelopment ( ) ? "licensing_dev.cer" : "licensing.cer" , null )
72- . GetAwaiter ( ) . GetResult ( ) ;
75+ X509Certificate2 devCert = null ;
76+ X509Certificate2 prodCert = CoreHelpers . GetEmbeddedCertificateAsync ( "licensing.cer" , null ) . GetAwaiter ( ) . GetResult ( ) ;
77+
78+ if ( environment . IsDevelopment ( ) )
79+ {
80+ devCert = CoreHelpers . GetEmbeddedCertificateAsync ( "licensing_dev.cer" , null ) . GetAwaiter ( ) . GetResult ( ) ;
81+ _creationCertificate = devCert ;
82+ // All self host envs accept prod cert. Creation cert added below to handle dev self-hosts
83+ _verificationCertificates . Add ( prodCert ) ;
84+ }
85+ else
86+ {
87+ _creationCertificate = prodCert ;
88+ }
89+
90+ // non-production environments can use dev cert-generated licenses
91+ if ( ! environment . IsProduction ( ) )
92+ {
93+ devCert ??= CoreHelpers . GetEmbeddedCertificateAsync ( "licensing_dev.cer" , null ) . GetAwaiter ( ) . GetResult ( ) ;
94+ _verificationCertificates . Add ( devCert ) ;
95+ }
7396 }
7497 else if ( CoreHelpers . SettingHasValue ( _globalSettings . Storage ? . ConnectionString ) &&
7598 CoreHelpers . SettingHasValue ( _globalSettings . LicenseCertificatePassword ) )
7699 {
77- _certificate = CoreHelpers . GetBlobCertificateAsync ( globalSettings . Storage . ConnectionString , "certificates" ,
100+ _creationCertificate = CoreHelpers . GetBlobCertificateAsync ( globalSettings . Storage . ConnectionString , "certificates" ,
78101 "licensing.pfx" , _globalSettings . LicenseCertificatePassword )
79102 . GetAwaiter ( ) . GetResult ( ) ;
80103 }
81104 else
82105 {
83- _certificate = CoreHelpers . GetCertificate ( certThumbprint ) ;
106+ _creationCertificate = CoreHelpers . GetCertificate ( creationCertThumbprint ) ;
84107 }
108+ // Creation cert can always be used to verify
109+ _verificationCertificates . Add ( _creationCertificate ) ;
85110
86- if ( _certificate == null || ! _certificate . Thumbprint . Equals ( CoreHelpers . CleanCertificateThumbprint ( certThumbprint ) ,
111+ if ( _creationCertificate == null || ! _creationCertificate . Thumbprint . Equals ( CoreHelpers . CleanCertificateThumbprint ( creationCertThumbprint ) ,
87112 StringComparison . InvariantCultureIgnoreCase ) )
88113 {
89114 throw new Exception ( "Invalid licensing certificate." ) ;
90115 }
116+ var allowedThumbprints = new HashSet < string > ( StringComparer . OrdinalIgnoreCase )
117+ {
118+ CoreHelpers . CleanCertificateThumbprint ( _productionCertThumbprint ) ,
119+ CoreHelpers . CleanCertificateThumbprint ( _developmentCertThumbprint )
120+ } ;
121+ if ( _verificationCertificates is null || _verificationCertificates . Count == 0
122+ || _verificationCertificates . Any ( c => ! allowedThumbprints . Contains ( c . Thumbprint ) ) )
123+ {
124+ throw new Exception ( "Invalid license verifying certificate." ) ;
125+ }
91126
92127 if ( _globalSettings . SelfHosted && ! CoreHelpers . SettingHasValue ( _globalSettings . LicenseDirectory ) )
93128 {
@@ -132,7 +167,7 @@ public async Task ValidateOrganizationsAsync()
132167 continue ;
133168 }
134169
135- if ( string . IsNullOrWhiteSpace ( license . Token ) && ! license . VerifySignature ( _certificate ) )
170+ if ( string . IsNullOrWhiteSpace ( license . Token ) && ! _verificationCertificates . Any ( c => license . VerifySignature ( c ) ) )
136171 {
137172 await DisableOrganizationAsync ( org , license , "Invalid signature." ) ;
138173 continue ;
@@ -231,7 +266,7 @@ private async Task<bool> ProcessUserValidationAsync(User user)
231266 return false ;
232267 }
233268
234- if ( string . IsNullOrWhiteSpace ( license . Token ) && ! license . VerifySignature ( _certificate ) )
269+ if ( string . IsNullOrWhiteSpace ( license . Token ) && ! _verificationCertificates . Any ( c => license . VerifySignature ( c ) ) )
235270 {
236271 await DisablePremiumAsync ( user , license , "Invalid signature." ) ;
237272 return false ;
@@ -271,7 +306,7 @@ public bool VerifyLicense(ILicense license)
271306 {
272307 if ( string . IsNullOrWhiteSpace ( license . Token ) )
273308 {
274- return license . VerifySignature ( _certificate ) ;
309+ return _verificationCertificates . Any ( ( c ) => license . VerifySignature ( c ) ) ;
275310 }
276311
277312 try
@@ -288,12 +323,12 @@ public bool VerifyLicense(ILicense license)
288323
289324 public byte [ ] SignLicense ( ILicense license )
290325 {
291- if ( _globalSettings . SelfHosted || ! _certificate . HasPrivateKey )
326+ if ( _globalSettings . SelfHosted || ! _creationCertificate . HasPrivateKey )
292327 {
293328 throw new InvalidOperationException ( "Cannot sign licenses." ) ;
294329 }
295330
296- return license . Sign ( _certificate ) ;
331+ return license . Sign ( _creationCertificate ) ;
297332 }
298333
299334 private UserLicense ReadUserLicense ( User user )
@@ -341,7 +376,7 @@ public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)
341376 var validationParameters = new TokenValidationParameters
342377 {
343378 ValidateIssuerSigningKey = true ,
344- IssuerSigningKey = new X509SecurityKey ( _certificate ) ,
379+ IssuerSigningKeys = _verificationCertificates . Select ( c => new X509SecurityKey ( c ) ) ,
345380 ValidateIssuer = true ,
346381 ValidIssuer = "bitwarden" ,
347382 ValidateAudience = true ,
@@ -393,7 +428,7 @@ private string GenerateToken(List<Claim> claims, string audience)
393428 claims . Add ( new Claim ( JwtClaimTypes . JwtId , Guid . NewGuid ( ) . ToString ( ) ) ) ;
394429 }
395430
396- var securityKey = new RsaSecurityKey ( _certificate . GetRSAPrivateKey ( ) ) ;
431+ var securityKey = new RsaSecurityKey ( _creationCertificate . GetRSAPrivateKey ( ) ) ;
397432 var tokenDescriptor = new SecurityTokenDescriptor
398433 {
399434 Subject = new ClaimsIdentity ( claims ) ,
0 commit comments