1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-22263] Integate Rust SDK to Seeder (#6150)

Adds a Rust SDK for performing seed related cryptograhic operations. It depends on internal portions of our Rust SDK. Primarily parts of the bitwarden-crypto crate.
This commit is contained in:
Oscar Hinton
2025-10-21 23:46:37 +02:00
committed by GitHub
parent 9c51c9971b
commit 44a82d3b22
17 changed files with 3651 additions and 0 deletions

3
.github/CODEOWNERS vendored
View File

@@ -96,6 +96,9 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
# The PushType enum is expected to be editted by anyone without need for Platform review
src/Core/Platform/Push/PushType.cs
# SDK
util/RustSdk @bitwarden/team-sdk-sme
# Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json
Directory.Build.props

View File

@@ -2,6 +2,7 @@
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies
enabledManagers: [
"cargo",
"dockerfile",
"docker-compose",
"github-actions",
@@ -9,6 +10,11 @@
"nuget",
],
packageRules: [
{
groupName: "cargo minor",
matchManagers: ["cargo"],
matchUpdateTypes: ["minor"],
},
{
groupName: "dockerfile minor",
matchManagers: ["dockerfile"],

View File

@@ -32,6 +32,14 @@ jobs:
- name: Set up .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
- name: Install rust
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
with:
toolchain: stable
- name: Cache cargo registry
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
- name: Print environment
run: |
dotnet --info

2
.gitignore vendored
View File

@@ -216,6 +216,8 @@ bitwarden_license/src/Sso/wwwroot/assets
.mono
src/Core/MailTemplates/Mjml/out
src/Core/MailTemplates/Mjml/out-hbs
NativeMethods.g.cs
util/RustSdk/rust/target
src/Admin/Admin.zip
src/Api/Api.zip

View File

@@ -133,6 +133,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
EndProject
Global
@@ -339,6 +340,10 @@ Global
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -397,6 +402,7 @@ Global
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View File

@@ -0,0 +1,58 @@
using System.Reflection;
using System.Runtime.InteropServices;
namespace Bit.RustSDK;
public static partial class NativeMethods
{
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform
// Library path will search
// win => __DllName, __DllName.dll
// linux, osx => __DllName.so, __DllName.dylib
static NativeMethods()
{
NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
}
static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName != __DllName) return IntPtr.Zero;
var path = "runtimes/";
var extension = "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
path += "win-";
extension = ".dll";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
path += "osx-";
extension = ".dylib";
}
else
{
path += "linux-";
extension = ".so";
}
if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
{
path += "x86";
}
else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
path += "x64";
}
else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
path += "arm64";
}
path += "/native/" + __DllName + extension;
return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath);
}
}

View File

@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Bit.RustSDK</RootNamespace>
<UserSecretsId>Bit.RustSDK</UserSecretsId>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Content Include="rust/target/release/libsdk*.dylib">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Link>runtimes/osx-arm64/native/libsdk.dylib</Link>
</Content>
<Content Include="./rust/target/release/libsdk*.so">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Link>runtimes/linux-x64/native/libsdk.dylib</Link>
</Content>
<Content Include="./rust/target/release/libsdk*.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Link>runtimes/windows-x64/native/libsdk.dylib</Link>
</Content>
<!-- This is a work around because this file is compiled by the PreBuild event below, and won't
always be detected -->
<Compile Remove="NativeMethods.g.cs" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="cargo build --release" WorkingDirectory="$(ProjectDir)/rust" />
<ItemGroup>
<Compile Include="NativeMethods.g.cs" />
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,19 @@
namespace Bit.RustSDK;
/// <summary>
/// Exception thrown when the Rust SDK operations fail
/// </summary>
public class RustSdkException : Exception
{
public RustSdkException() : base("An error occurred in the Rust SDK operation")
{
}
public RustSdkException(string message) : base(message)
{
}
public RustSdkException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -0,0 +1,104 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace Bit.RustSDK;
public class UserKeys
{
public required string MasterPasswordHash { get; set; }
/// <summary>
/// Base64 encoded UserKey
/// </summary>
public required string Key { get; set; }
public required string EncryptedUserKey { get; set; }
public required string PublicKey { get; set; }
public required string PrivateKey { get; set; }
}
public class OrganizationKeys
{
/// <summary>
/// Base64 encoded SymmetricCryptoKey
/// </summary>
public required string Key { get; set; }
public required string PublicKey { get; set; }
public required string PrivateKey { get; set; }
}
/// <summary>
/// Service implementation that provides a C# friendly interface to the Rust SDK
/// </summary>
public class RustSdkService
{
private static readonly JsonSerializerOptions CaseInsensitiveOptions = new()
{
PropertyNameCaseInsensitive = true
};
public unsafe UserKeys GenerateUserKeys(string email, string password)
{
var emailBytes = StringToRustString(email);
var passwordBytes = StringToRustString(password);
fixed (byte* emailPtr = emailBytes)
fixed (byte* passwordPtr = passwordBytes)
{
var resultPtr = NativeMethods.generate_user_keys(emailPtr, passwordPtr);
var result = TakeAndDestroyRustString(resultPtr);
return JsonSerializer.Deserialize<UserKeys>(result, CaseInsensitiveOptions)!;
}
}
public unsafe OrganizationKeys GenerateOrganizationKeys()
{
var resultPtr = NativeMethods.generate_organization_keys();
var result = TakeAndDestroyRustString(resultPtr);
return JsonSerializer.Deserialize<OrganizationKeys>(result, CaseInsensitiveOptions)!;
}
public unsafe string GenerateUserOrganizationKey(string userKey, string orgKey)
{
var userKeyBytes = StringToRustString(userKey);
var orgKeyBytes = StringToRustString(orgKey);
fixed (byte* userKeyPtr = userKeyBytes)
fixed (byte* orgKeyPtr = orgKeyBytes)
{
var resultPtr = NativeMethods.generate_user_organization_key(userKeyPtr, orgKeyPtr);
var result = TakeAndDestroyRustString(resultPtr);
return result;
}
}
private static byte[] StringToRustString(string str)
{
return Encoding.UTF8.GetBytes(str + '\0');
}
private static unsafe string TakeAndDestroyRustString(byte* ptr)
{
if (ptr == null)
{
throw new RustSdkException("Pointer is null");
}
var result = Marshal.PtrToStringUTF8((IntPtr)ptr);
NativeMethods.free_c_string(ptr);
if (result == null)
{
throw new RustSdkException("Failed to convert native result to string");
}
return result;
}
}

View File

@@ -0,0 +1,21 @@
namespace Bit.RustSDK;
/// <summary>
/// Factory for creating Rust SDK service instances
/// </summary>
public static class RustSdkServiceFactory
{
/// <summary>
/// Creates a singleton instance of the Rust SDK service (thread-safe)
/// </summary>
/// <returns>A singleton IRustSdkService instance</returns>
public static RustSdkService CreateSingleton()
{
return SingletonHolder.Instance;
}
private static class SingletonHolder
{
internal static readonly RustSdkService Instance = new();
}
}

3147
util/RustSdk/rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
[package]
name = "sdk"
publish = false
version = "0.1.0"
authors = ["Bitwarden Inc"]
edition = "2021"
homepage = "https://bitwarden.com"
repository = "https://github.com/bitwarden/server"
[lib]
crate-type = ["cdylib"]
[dependencies]
base64 = "0.22.1"
bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "1461b3ba6bb6e2d0114770eb4572a1398b4789ef" }
bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "1461b3ba6bb6e2d0114770eb4572a1398b4789ef" }
serde = "=1.0.219"
serde_json = "=1.0.141"
[build-dependencies]
csbindgen = "=1.9.3"
# Compile all dependencies with some optimizations when building this crate on debug
# This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster
# As clean builds won't occur very often, this won't slow down the development process
[profile.dev.package."*"]
opt-level = 2
[profile.release]
codegen-units = 1
lto = true
opt-level = 3

View File

@@ -0,0 +1,9 @@
fn main() {
csbindgen::Builder::default()
.input_extern_file("src/lib.rs")
.csharp_dll_name("libsdk")
.csharp_namespace("Bit.RustSDK")
.csharp_class_accessibility("public")
.generate_csharp_file("../NativeMethods.g.cs")
.unwrap();
}

View File

@@ -0,0 +1,152 @@
#![allow(clippy::missing_safety_doc)]
use std::{
ffi::{c_char, CStr, CString},
num::NonZeroU32,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose, Kdf,
KeyEncryptable, MasterKey, RsaKeyPair, SpkiPublicKeyBytes, SymmetricCryptoKey,
UnsignedSharedKey, UserKey,
};
#[no_mangle]
pub unsafe extern "C" fn generate_user_keys(
email: *const c_char,
password: *const c_char,
) -> *const c_char {
let email = CStr::from_ptr(email).to_str().unwrap();
let password = CStr::from_ptr(password).to_str().unwrap();
println!("Generating keys for {email}");
println!("Password: {password}");
let kdf = Kdf::PBKDF2 {
iterations: NonZeroU32::new(5_000).unwrap(),
};
let master_key = MasterKey::derive(password, email, &kdf).unwrap();
let master_password_hash =
master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization);
println!("Master password hash: {}", master_password_hash);
let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap();
let keypair = keypair(&user_key.0);
let json = serde_json::json!({
"masterPasswordHash": master_password_hash,
"key": user_key.0.to_base64(),
"encryptedUserKey": encrypted_user_key.to_string(),
"publicKey": keypair.public.to_string(),
"privateKey": keypair.private.to_string(),
})
.to_string();
let result = CString::new(json).unwrap();
result.into_raw()
}
fn keypair(key: &SymmetricCryptoKey) -> RsaKeyPair {
const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
4LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa
F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A
QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16
l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq
92qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP
dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a
PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX
UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR
buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2
hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc
vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA
hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs
tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY
UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs
zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX
E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP
jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez
MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9
JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg
AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp
Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
XKZBokBGnjFnTnKcs7nv/O8=
-----END PRIVATE KEY-----";
let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
let public_key = private_key.to_public_key().to_der().unwrap();
let p = private_key.to_der().unwrap();
RsaKeyPair {
private: p.encrypt_with_key(key).unwrap(),
public: public_key.into(),
}
}
#[no_mangle]
pub unsafe extern "C" fn generate_organization_keys() -> *const c_char {
let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
let key = UserKey::new(key);
let keypair = key.make_key_pair().expect("Failed to generate key pair");
let json = serde_json::json!({
"key": key.0.to_base64(),
"publicKey": keypair.public.to_string(),
"privateKey": keypair.private.to_string(),
})
.to_string();
let result = CString::new(json).unwrap();
result.into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn generate_user_organization_key(
user_public_key: *const c_char,
organization_key: *const c_char,
) -> *const c_char {
let user_public_key = CStr::from_ptr(user_public_key).to_str().unwrap().to_owned();
let organization_key = CStr::from_ptr(organization_key)
.to_str()
.unwrap()
.to_owned();
let user_public_key = STANDARD.decode(user_public_key).unwrap();
let organization_key = STANDARD.decode(organization_key).unwrap();
let encapsulation_key =
AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(user_public_key)).unwrap();
let encrypted_key = UnsignedSharedKey::encapsulate_key_unsigned(
&SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(organization_key)).unwrap(),
&encapsulation_key,
)
.unwrap();
let result = CString::new(encrypted_key.to_string()).unwrap();
result.into_raw()
}
/// # Safety
///
/// The `str` pointer must be a valid pointer previously returned by `CString::into_raw`
/// and must not have already been freed. After calling this function, the pointer must not be used again.
#[no_mangle]
pub unsafe extern "C" fn free_c_string(str: *mut c_char) {
unsafe {
drop(CString::from_raw(str));
}
}

View File

@@ -41,4 +41,18 @@ public static class OrgnaizationExtensions
Status = OrganizationUserStatusType.Confirmed
};
}
public static OrganizationUser CreateSdkOrganizationUser(this Organization organization, User user)
{
return new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
UserId = user.Id,
Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==",
Type = OrganizationUserType.Admin,
Status = OrganizationUserStatusType.Confirmed
};
}
}

View File

@@ -1,5 +1,7 @@
using Bit.Core.Enums;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.RustSDK;
using Microsoft.AspNetCore.Identity;
namespace Bit.Seeder.Factories;
@@ -22,4 +24,29 @@ public class UserSeeder
KdfIterations = 600_000,
};
}
public static (User user, string userKey) CreateSdkUser(IPasswordHasher<Bit.Core.Entities.User> passwordHasher, string email)
{
var nativeService = RustSdkServiceFactory.CreateSingleton();
var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf");
var user = new User
{
Id = Guid.NewGuid(),
Email = email,
MasterPassword = null,
SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609",
Key = keys.EncryptedUserKey,
PublicKey = keys.PublicKey,
PrivateKey = keys.PrivateKey,
ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 5_000,
};
user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash);
return (user, keys.Key);
}
}

View File

@@ -20,6 +20,7 @@
<ProjectReference Include="..\..\src\Core\Core.csproj" />
<ProjectReference Include="..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
<ProjectReference Include="..\..\src\SharedWeb\SharedWeb.csproj" />
<ProjectReference Include="..\RustSdk\RustSdk.csproj" />
</ItemGroup>
<ItemGroup>