Update to .NET 10.0 and fix certificate validation for Bitwarden server 2026.5.0 (#282)

* Update to .NET 10.0 for Bitwarden server 2026.5.0 compatibility

Bitwarden server 2026.5.0 ships with .NET 10.0 runtime only, breaking
the fast-patch build. This commit updates all .NET projects and build
pipelines to target net10.0 and the dotnet/sdk:10.0 image.

Additionally:
- Replace obsolete X509Certificate2(byte[]) constructors with
  X509CertificateLoader.LoadCertificate() / LoadPkcs12FromFile()
  to resolve SYSLIB0057 warnings introduced in .NET 9/10
- Add -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg SHA256 to
  generate-keys.sh PKCS#12 export, fixing OpenSSL 3.x errors caused
  by the deprecated RC2-40-CBC legacy algorithm
- Update FixRuntimeConfig fallback framework version to 10.0.0

Fixes #281

Signed-off-by: Pascal Pothmann <19438422+p0thi@users.noreply.github.com>

* Fix certificate validation by replacing all thumbprint occurrences

Bitwarden's LicensingService performs two validation checks:
1. Validates _creationCertificate thumbprint
2. Validates all certificates in _verificationCertificates

The thumbprint constants are inlined at compile time, creating multiple
Ldstr instructions in the IL code. The patcher was only replacing the
first occurrence, causing the second validation to fail with:
'Invalid license verifying certificate.'

This fix replaces ALL occurrences of the old thumbprint to ensure both
validation checks pass.

Fixes runtime error: 'Invalid license verifying certificate'

---------

Signed-off-by: Pascal Pothmann <19438422+p0thi@users.noreply.github.com>
Co-authored-by: Pascal Pothmann <19438422+p0thi@users.noreply.github.com>
This commit is contained in:
Pascal Pothmann
2026-06-03 21:57:45 +02:00
committed by GitHub
parent 9962717481
commit 8def331bb6
9 changed files with 28 additions and 21 deletions

View File

@@ -15,6 +15,6 @@ DIR=`exec 2>/dev/null;(cd -- "$DIR") && cd -- "$DIR"|| cd "$DIR"; unset PWD; /us
# Generate new keys
openssl req -x509 -newkey rsa:4096 -keyout "$DIR/key.pem" -out "$DIR/cert.cert" -days 36500 -subj '/CN=www.mydom.com/O=My Company Name LTD./C=US' -outform DER -passout pass:test
openssl x509 -inform DER -in "$DIR/cert.cert" -out "$DIR/cert.pem"
openssl pkcs12 -export -out "$DIR/cert.pfx" -inkey "$DIR/key.pem" -in "$DIR/cert.pem" -passin pass:test -passout pass:test
openssl pkcs12 -export -out "$DIR/cert.pfx" -inkey "$DIR/key.pem" -in "$DIR/cert.pem" -passin pass:test -passout pass:test -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg SHA256
ls

View File

@@ -70,7 +70,7 @@ else
docker run --rm \
-v "$DIR/src/bitBetter:/bitBetter" \
-w /bitBetter \
mcr.microsoft.com/dotnet/sdk:8.0 sh build.sh
mcr.microsoft.com/dotnet/sdk:10.0 sh build.sh
docker build \
--no-cache \

View File

@@ -1,7 +1,7 @@
ARG BITWARDEN_TAG
FROM ${BITWARDEN_TAG}
COPY bin/Release/net8.0/publish/* /bitBetter/
COPY bin/Release/net10.0/publish/* /bitBetter/
COPY ./.keys/cert.cert /newLicensing.cer
RUN set -e; set -x; \

View File

@@ -66,8 +66,8 @@ namespace BitwardenSelfLicensor
module.Resources.Add(new EmbeddedResource("Bit.Core.licensing.cer", existingRes.Attributes, certBytes));
module.Resources.Remove(existingRes);
var existingCert = new X509Certificate2(existingRes.GetResourceData());
var newCert = new X509Certificate2(certBytes);
var existingCert = X509CertificateLoader.LoadCertificate(existingRes.GetResourceData());
var newCert = X509CertificateLoader.LoadCertificate(certBytes);
Console.WriteLine($"Old thumbprint: {existingCert.Thumbprint}");
Console.WriteLine($"New thumbprint: {newCert.Thumbprint}");
@@ -85,15 +85,22 @@ namespace BitwardenSelfLicensor
// Use Contains() to handle the hidden Unicode LRM character (\u200E) that Bitwarden
// prepends to the production thumbprint string literal in LicensingService.cs
var instToReplace = ctor.Body.Instructions
// Replace ALL occurrences since const fields are inlined at compile time and used in
// multiple validation checks (both _creationCertificate and _verificationCertificates)
var instructionsToReplace = ctor.Body.Instructions
.Where(i => i.OpCode == OpCodes.Ldstr)
.FirstOrDefault(i => ((string)i.Operand)
.Contains(existingCert.Thumbprint, StringComparison.OrdinalIgnoreCase));
.Where(i => ((string)i.Operand)
.Contains(existingCert.Thumbprint, StringComparison.OrdinalIgnoreCase))
.ToList();
if (instToReplace != null)
if (instructionsToReplace.Count > 0)
{
Console.WriteLine($"Replacing thumbprint Ldstr: '{instToReplace.Operand}'");
rewriter.Replace(instToReplace, Instruction.Create(OpCodes.Ldstr, newCert.Thumbprint));
Console.WriteLine($"Found {instructionsToReplace.Count} thumbprint Ldstr instruction(s) to replace");
foreach (var inst in instructionsToReplace)
{
Console.WriteLine($" Replacing: '{inst.Operand}'");
rewriter.Replace(inst, Instruction.Create(OpCodes.Ldstr, newCert.Thumbprint));
}
}
else
{
@@ -121,7 +128,7 @@ namespace BitwardenSelfLicensor
// Derive framework name/version from the self-contained includedFrameworks before removing it
string fwName = "Microsoft.AspNetCore.App";
string fwVersion = "8.0.0";
string fwVersion = "10.0.0";
if (opts["includedFrameworks"] is JsonArray included && included.Count > 0)
{
var first = included[0]!.AsObject();

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -4,4 +4,4 @@ set -e
set -x
dotnet restore
dotnet publish -c Release -o bin/Release/net8.0/publish
dotnet publish -c Release -o bin/Release/net10.0/publish

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /licenseGen
@@ -12,6 +12,6 @@ RUN set -e; set -x; \
FROM bitbetter/api
COPY --from=build /licenseGen/bin/Release/net8.0/publish/* /app/
COPY --from=build /licenseGen/bin/Release/net10.0/publish/* /app/
ENTRYPOINT [ "dotnet", "/app/licenseGen.dll", "--core", "/app/Core.dll", "--executable", "/app/Api", "--cert", "/cert.pfx" ]

View File

@@ -141,7 +141,7 @@ namespace BitwardenSelfLicensor
buff = Console.ReadLine();
if ( buff == "" || buff == "y" || buff == "Y" )
{
GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name, email, storage, guid, null);
GenerateUserLicense(X509CertificateLoader.LoadPkcs12FromFile(cert.Value(), "test"), GetCoreDllPath(), name, email, storage, guid, null);
}
else
{
@@ -155,7 +155,7 @@ namespace BitwardenSelfLicensor
buff = Console.ReadLine();
if ( buff == "" || buff == "y" || buff == "Y" )
{
GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name, email, storage, installid, businessname, null);
GenerateOrgLicense(X509CertificateLoader.LoadPkcs12FromFile(cert.Value(), "test"), GetCoreDllPath(), name, email, storage, installid, businessname, null);
}
else
{
@@ -214,7 +214,7 @@ namespace BitwardenSelfLicensor
storageShort = (short) parsedStorage;
}
GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name.Value, email.Value, storageShort, userId, key.Value);
GenerateUserLicense(X509CertificateLoader.LoadPkcs12FromFile(cert.Value(), "test"), GetCoreDllPath(), name.Value, email.Value, storageShort, userId, key.Value);
return 0;
});
@@ -269,7 +269,7 @@ namespace BitwardenSelfLicensor
storageShort = (short) parsedStorage;
}
GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name.Value, email.Value, storageShort, installationId, businessName.Value, key.Value);
GenerateOrgLicense(X509CertificateLoader.LoadPkcs12FromFile(cert.Value(), "test"), GetCoreDllPath(), name.Value, email.Value, storageShort, installationId, businessName.Value, key.Value);
return 0;
});

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>