Skip to content

Commit bbead40

Browse files
committed
fix(shared): remove libsodium device identity dependency
1 parent 913ba4e commit bbead40

9 files changed

Lines changed: 72 additions & 69 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,9 @@ jobs:
397397
if: matrix.rid == 'win-arm64'
398398
shell: pwsh
399399
# -SkipNativeLoadProbe: an ARM64 runner CAN LoadLibrary ARM64 DLLs, but
400-
# libsodium pulls in a long native dependency chain that may not all be
401-
# in the payload here (WindowsAppSDK pieces, etc.). The probe is for x64
402-
# parity; signature + presence is what we actually care about.
400+
# the full tray native dependency chain may include components that are
401+
# not in this payload here (WindowsAppSDK pieces, etc.). The probe is for
402+
# x64 parity; signature + presence is what we actually care about.
403403
run: .\scripts\Test-ReleaseNativeDependencies.ps1 -PayloadPath publish -RequireAppLocalVCRuntime -SkipNativeLoadProbe
404404

405405
- name: Verify GitVersion assembly metadata
@@ -635,8 +635,8 @@ jobs:
635635
}
636636
637637
# Create ZIP files for Updatum auto-update (asset name must contain the RID).
638-
# We ship both x64 and arm64 portables now that the build job produces a
639-
# libsodium-compatible app-local VC runtime for both architectures (the
638+
# We ship both x64 and arm64 portables now that the build job produces an
639+
# app-local VC runtime for both architectures (the
640640
# arm64 leg sources its loose Microsoft.VC*.CRT DLLs from the VS install
641641
# on the windows-11-arm runner; see src/Directory.Build.targets).
642642
- name: Create x64 Release ZIP

docs/RELEASING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ verifier fails closed on unknown `.exe` files so future payload changes are
107107
reviewed deliberately.
108108

109109
CI also checks native runtime dependencies before release packaging. Both the
110-
x64 and ARM64 portable payloads must ship `vcruntime140.dll` next to every
111-
`libsodium.dll` copy. Both build legs source their loose VC runtime DLLs from
112-
the Visual Studio install on the CI runner (resolved via `vswhere` in
110+
x64 and ARM64 portable payloads must ship `vcruntime140.dll` in the payload
111+
root for the native speech stack. Both build legs source their loose VC runtime
112+
DLLs from the Visual Studio install on the CI runner (resolved via `vswhere` in
113113
`src\Directory.Build.targets`). This ensures the bundled CRT is new enough for
114114
`onnxruntime` — the `VCRuntime.CefSharp.140` NuGet is only used as a dev-time
115115
convenience for local `dotnet build` (not publish). The release validation
@@ -121,7 +121,7 @@ The release job must Authenticode-verify Microsoft's x64 and ARM64 Visual C++
121121
Runtime redistributables before passing the
122122
architecture-matching redistributable to Inno. The installer runs the
123123
redistributable before launching the tray so clean or stale Windows hosts can
124-
repair the runtime before Ed25519 device keys are generated or loaded, and it
124+
repair the runtime before native speech components initialize, and it
125125
skips the post-install tray launch if the runtime installer fails.
126126

127127
The current Azure Artifact Signing resource is:

scripts/Test-ReleaseNativeDependencies.ps1

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
Verifies native release payload dependencies needed on clean Windows hosts.
44
55
.DESCRIPTION
6-
The Windows node uses NSec.Cryptography, which loads libsodium.dll. The
7-
NuGet-provided Windows libsodium binary imports the Visual C++ runtime, so
8-
app-local/installer payloads must make that runtime available before the
9-
tray can generate or load device keys.
6+
The Windows node ships native speech dependencies that import the Visual C++
7+
runtime. Release payloads and installers must make that runtime available on
8+
clean Windows hosts before the tray initializes those native components.
109
#>
1110
[CmdletBinding()]
1211
param(
@@ -100,7 +99,6 @@ function Get-NativeLoadProbeFiles {
10099
)
101100

102101
@(
103-
Get-ChildItem -LiteralPath $Directory -File -Filter "libsodium.dll"
104102
Get-ChildItem -LiteralPath $Directory -File -Filter "onnxruntime.dll"
105103
Get-ChildItem -LiteralPath $Directory -File -Filter "sherpa-onnx.dll"
106104
Get-ChildItem -LiteralPath $Directory -File -Filter "sherpa-onnx-c-api.dll"
@@ -293,26 +291,15 @@ function Add-VCRuntimeVersionFloorErrors {
293291
}
294292
}
295293

296-
$libsodiumFiles = @(
297-
Get-ChildItem -LiteralPath $payloadRoot -Recurse -File -Filter libsodium.dll |
298-
Sort-Object FullName
299-
)
300-
301-
if ($libsodiumFiles.Count -eq 0) {
302-
$errors.Add("Missing libsodium.dll under $payloadRoot.")
303-
}
304-
305-
foreach ($libsodium in $libsodiumFiles) {
306-
$runtimePath = Join-Path $libsodium.DirectoryName "vcruntime140.dll"
307-
if ($RequireAppLocalVCRuntime -and -not (Test-Path -LiteralPath $runtimePath)) {
308-
$errors.Add("Missing app-local vcruntime140.dll next to $(Get-RelativePath -Root $payloadRoot -Path $libsodium.FullName).")
294+
if ($RequireAppLocalVCRuntime) {
295+
$runtimePath = Join-Path $payloadRoot "vcruntime140.dll"
296+
if (-not (Test-Path -LiteralPath $runtimePath)) {
297+
$errors.Add("Missing app-local vcruntime140.dll under $payloadRoot.")
309298
}
310299

311-
if ($RequireAppLocalVCRuntime) {
312-
foreach ($runtimeFile in Get-VCRuntimeFiles -Directory $libsodium.DirectoryName) {
313-
Add-MicrosoftSignatureErrors -File $runtimeFile
314-
Add-VCRuntimeVersionFloorErrors -File $runtimeFile
315-
}
300+
foreach ($runtimeFile in Get-VCRuntimeFiles -Directory $payloadRoot) {
301+
Add-MicrosoftSignatureErrors -File $runtimeFile
302+
Add-VCRuntimeVersionFloorErrors -File $runtimeFile
316303
}
317304
}
318305

src/Directory.Build.targets

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@
4949
major version (Microsoft.VC143.CRT for VS 2022, Microsoft.VC145.CRT for
5050
VS 18). There is at most one Microsoft.VC*.CRT subdir per arch per redist
5151
version, so the * collapses to a single match. We limit to
52-
vcruntime140*.dll + msvcp140*.dll to match the file set the x64 NuGet
53-
ships (concrt140/vccorlib140/vcruntime140_threads aren't needed by
54-
libsodium and aren't in the x64 payload either).
52+
vcruntime140*.dll + msvcp140*.dll to match the file set required by the
53+
native speech stack without carrying unrelated CRT DLLs.
5554
5655
For x64 this replaces the stale NuGet-sourced items so publish always
5756
ships current DLLs from the VS install.
@@ -102,7 +101,7 @@
102101
AfterTargets="CopyOpenClawVCRuntimeToPublish"
103102
Condition="'$(PublishDir)' != '' and '$(OpenClawVCRuntimeArch)' != ''">
104103
<Error Condition="!Exists('$(PublishDir)vcruntime140.dll')"
105-
Text="vcruntime140.dll missing from $(PublishDir). Clean Windows installs cannot load libsodium.dll without the VC runtime." />
104+
Text="vcruntime140.dll missing from $(PublishDir). Clean Windows installs cannot load native speech dependencies without the VC runtime." />
106105
</Target>
107106

108107
</Project>

src/OpenClaw.Shared/DeviceIdentity.cs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Text;
77
using System.Text.Json;
88
using OpenClaw.Shared.Mcp;
9-
using NSec.Cryptography;
9+
using Org.BouncyCastle.Math.EC.Rfc8032;
1010

1111
namespace OpenClaw.Shared;
1212

@@ -17,18 +17,16 @@ public class DeviceIdentity
1717
{
1818
private readonly string _keyPath;
1919
private readonly IOpenClawLogger _logger;
20-
private Key? _privateKey;
21-
private PublicKey? _publicKey;
20+
private byte[]? _privateKey;
21+
private byte[]? _publicKey;
2222
private string? _deviceId;
2323
private string? _deviceToken;
2424
private string[]? _deviceTokenScopes;
2525
private string? _nodeDeviceToken;
2626
private string[]? _nodeDeviceTokenScopes;
2727

28-
private static readonly SignatureAlgorithm Ed25519Algorithm = SignatureAlgorithm.Ed25519;
29-
3028
public string DeviceId => _deviceId ?? throw new InvalidOperationException("Device not initialized");
31-
public string PublicKeyBase64Url => _publicKey != null ? Base64UrlEncode(_publicKey.Export(KeyBlobFormat.RawPublicKey)) : throw new InvalidOperationException("Device not initialized");
29+
public string PublicKeyBase64Url => _publicKey != null ? Base64UrlEncode(_publicKey) : throw new InvalidOperationException("Device not initialized");
3230
public string? DeviceToken => _deviceToken;
3331
public IReadOnlyList<string>? DeviceTokenScopes => _deviceTokenScopes;
3432
public string? NodeDeviceToken => _nodeDeviceToken;
@@ -192,9 +190,9 @@ private void LoadExisting()
192190
return;
193191
}
194192

195-
var privateKeyBytes = Convert.FromBase64String(data.PrivateKeyBase64);
196-
_privateKey = Key.Import(Ed25519Algorithm, privateKeyBytes, KeyBlobFormat.RawPrivateKey);
197-
_publicKey = _privateKey.PublicKey;
193+
_privateKey = Convert.FromBase64String(data.PrivateKeyBase64);
194+
_publicKey = new byte[Ed25519.PublicKeySize];
195+
Ed25519.GeneratePublicKey(_privateKey, 0, _publicKey, 0);
198196
_deviceId = data.DeviceId;
199197
_deviceToken = data.DeviceToken;
200198
_deviceTokenScopes = NormalizeScopes(data.DeviceTokenScopes);
@@ -214,20 +212,21 @@ private void GenerateNew()
214212
{
215213
_logger.Info("Generating new Ed25519 device keypair...");
216214

217-
// Generate Ed25519 keypair using NSec
218-
_privateKey = Key.Create(Ed25519Algorithm, new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport });
219-
_publicKey = _privateKey.PublicKey;
215+
_privateKey = new byte[Ed25519.SecretKeySize];
216+
RandomNumberGenerator.Fill(_privateKey);
217+
_publicKey = new byte[Ed25519.PublicKeySize];
218+
Ed25519.GeneratePublicKey(_privateKey, 0, _publicKey, 0);
220219

221220
// Get raw 32-byte public key
222-
var publicKeyBytes = _publicKey.Export(KeyBlobFormat.RawPublicKey);
221+
var publicKeyBytes = _publicKey;
223222

224223
// Device ID is SHA256 hash of raw 32-byte public key (hex encoded)
225224
using var sha256 = SHA256.Create();
226225
var hashBytes = sha256.ComputeHash(publicKeyBytes);
227226
_deviceId = Convert.ToHexString(hashBytes).ToLowerInvariant();
228227

229228
// Export private key for storage
230-
var privateKeyBytes = _privateKey.Export(KeyBlobFormat.RawPrivateKey);
229+
var privateKeyBytes = _privateKey;
231230

232231
// Save to disk
233232
var data = new DeviceKeyData
@@ -268,7 +267,7 @@ public string SignPayload(string nonce, long signedAtMs, string clientId, string
268267

269268
// Sign with Ed25519
270269
var dataBytes = Encoding.UTF8.GetBytes(payload);
271-
var signature = Ed25519Algorithm.Sign(_privateKey, dataBytes);
270+
var signature = SignEd25519(dataBytes);
272271

273272
// Return base64url encoded signature
274273
return Base64UrlEncode(signature);
@@ -304,7 +303,7 @@ public string SignConnectPayloadV3(
304303
deviceFamily);
305304

306305
var dataBytes = Encoding.UTF8.GetBytes(payload);
307-
var signature = Ed25519Algorithm.Sign(_privateKey, dataBytes);
306+
var signature = SignEd25519(dataBytes);
308307
return Base64UrlEncode(signature);
309308
}
310309

@@ -376,7 +375,7 @@ public string SignConnectPayloadV2(
376375
authToken);
377376

378377
var dataBytes = Encoding.UTF8.GetBytes(payload);
379-
var signature = Ed25519Algorithm.Sign(_privateKey, dataBytes);
378+
var signature = SignEd25519(dataBytes);
380379
return Base64UrlEncode(signature);
381380
}
382381

@@ -563,6 +562,16 @@ private static string DescribeException(Exception ex)
563562
? message
564563
: $"{message} (inner {ex.InnerException.GetType().Name}: {ex.InnerException.Message})";
565564
}
565+
566+
private byte[] SignEd25519(byte[] data)
567+
{
568+
if (_privateKey == null)
569+
throw new InvalidOperationException("Device not initialized");
570+
571+
var signature = new byte[Ed25519.SignatureSize];
572+
Ed25519.Sign(_privateKey, 0, data, 0, data.Length, signature, 0);
573+
return signature;
574+
}
566575

567576
private static string Base64UrlEncode(byte[] data)
568577
{

src/OpenClaw.Shared/OpenClaw.Shared.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</ItemGroup>
1414

1515
<ItemGroup>
16-
<PackageReference Include="NSec.Cryptography" Version="25.4.0" />
16+
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
1717
</ItemGroup>
1818

1919
<!-- Audio / Speech-to-Text (platform-agnostic components) -->
@@ -25,4 +25,3 @@
2525

2626
</Project>
2727

28-

tests/Directory.Build.props

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
<Nullable>enable</Nullable>
1212
<IsPackable>false</IsPackable>
1313
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
14-
<OpenClawLibsodiumVersion>1.0.20.1</OpenClawLibsodiumVersion>
1514
<OpenClawVCRuntimeVersion>1.0.5</OpenClawVCRuntimeVersion>
1615
<!-- Audit direct + transitive dependencies for known CVEs during restore,
1716
matching the setting in src/Directory.Build.props. -->
@@ -36,13 +35,11 @@
3635
<Using Include="Xunit" />
3736
</ItemGroup>
3837

39-
<Target Name="CopyWindowsNativeCryptoForTestHost" AfterTargets="Build" Condition="'$(OS)' == 'Windows_NT'">
38+
<Target Name="CopyWindowsNativeRuntimeForTestHost" AfterTargets="Build" Condition="'$(OS)' == 'Windows_NT'">
4039
<ItemGroup>
41-
<OpenClawNativeCrypto Include="$(NuGetPackageRoot)libsodium\$(OpenClawLibsodiumVersion)\runtimes\win-x64\native\libsodium.dll" Condition="'$(PROCESSOR_ARCHITECTURE)' != 'ARM64' And Exists('$(NuGetPackageRoot)libsodium\$(OpenClawLibsodiumVersion)\runtimes\win-x64\native\libsodium.dll')" />
42-
<OpenClawNativeCrypto Include="$(NuGetPackageRoot)libsodium\$(OpenClawLibsodiumVersion)\runtimes\win-arm64\native\libsodium.dll" Condition="'$(PROCESSOR_ARCHITECTURE)' == 'ARM64' And Exists('$(NuGetPackageRoot)libsodium\$(OpenClawLibsodiumVersion)\runtimes\win-arm64\native\libsodium.dll')" />
43-
<OpenClawNativeCrypto Include="$(NuGetPackageRoot)vcruntime.cefsharp.140\$(OpenClawVCRuntimeVersion)\vc_redist\x64\*.dll" Condition="'$(PROCESSOR_ARCHITECTURE)' != 'ARM64' And Exists('$(NuGetPackageRoot)vcruntime.cefsharp.140\$(OpenClawVCRuntimeVersion)\vc_redist\x64\vcruntime140.dll')" />
40+
<OpenClawNativeRuntime Include="$(NuGetPackageRoot)vcruntime.cefsharp.140\$(OpenClawVCRuntimeVersion)\vc_redist\x64\*.dll" Condition="'$(PROCESSOR_ARCHITECTURE)' != 'ARM64' And Exists('$(NuGetPackageRoot)vcruntime.cefsharp.140\$(OpenClawVCRuntimeVersion)\vc_redist\x64\vcruntime140.dll')" />
4441
</ItemGroup>
45-
<Copy SourceFiles="@(OpenClawNativeCrypto)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" />
42+
<Copy SourceFiles="@(OpenClawNativeRuntime)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" />
4643
</Target>
4744

4845
</Project>

tests/OpenClaw.Shared.Tests/DeviceIdentityTests.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Text;
44
using Xunit;
55
using OpenClaw.Shared;
6+
using Org.BouncyCastle.Math.EC.Rfc8032;
67

78
// slopwatch-ignore: SW002 Test intentionally disables obsolete warnings while covering legacy DeviceIdentity behavior.
89
#pragma warning disable CS0618 // Obsolete - testing legacy methods
@@ -22,6 +23,18 @@ private static string CreateTempDir()
2223
return dir;
2324
}
2425

26+
private static byte[] DecodeBase64Url(string value)
27+
{
28+
var padded = value.Replace('-', '+').Replace('_', '/');
29+
switch (padded.Length % 4)
30+
{
31+
case 2: padded += "=="; break;
32+
case 3: padded += "="; break;
33+
}
34+
35+
return Convert.FromBase64String(padded);
36+
}
37+
2538
[IntegrationFact]
2639
public void Initialize_GeneratesNewKeypair()
2740
{
@@ -76,6 +89,11 @@ public void SignPayload_ProducesDeterministicSignature()
7689
Assert.NotEmpty(sig1);
7790
// Ed25519 signature is 64 bytes → base64url is 86 chars (no padding)
7891
Assert.Equal(86, sig1.Length);
92+
93+
var publicKey = DecodeBase64Url(identity.PublicKeyBase64Url);
94+
var signature = DecodeBase64Url(sig1);
95+
var payloadBytes = Encoding.UTF8.GetBytes(identity.BuildDebugPayload("nonce1", 1000, "node-host", "tok"));
96+
Assert.True(Ed25519.Verify(signature, 0, publicKey, 0, payloadBytes, 0, payloadBytes.Length));
7997
}
8098
finally { Directory.Delete(dir, true); }
8199
}
@@ -370,13 +388,7 @@ public void PublicKeyBase64Url_IsValidBase64Url()
370388
Assert.DoesNotContain("=", pubKey);
371389

372390
// Decode and verify Ed25519 public key is exactly 32 bytes
373-
var padded = pubKey.Replace('-', '+').Replace('_', '/');
374-
switch (padded.Length % 4)
375-
{
376-
case 2: padded += "=="; break;
377-
case 3: padded += "="; break;
378-
}
379-
var bytes = Convert.FromBase64String(padded);
391+
var bytes = DecodeBase64Url(pubKey);
380392
Assert.Equal(32, bytes.Length);
381393
}
382394
finally { Directory.Delete(dir, true); }

tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void ReleaseWorkflow_BundlesAndVerifiesNativeRuntimeDependencies()
8282
Assert.Contains("Get-AuthenticodeSignature -LiteralPath $File.FullName", verifier);
8383
Assert.Contains("Get-VCRuntimeFiles", verifier);
8484
Assert.Contains("vcruntime140.dll", verifier);
85-
Assert.Contains("libsodium.dll", verifier);
85+
Assert.DoesNotContain("libsodium.dll", verifier);
8686
Assert.Contains("OpenClawNativeDependencyProbe", verifier);
8787
Assert.Contains("Microsoft.ML.OnnxRuntime.dll", verifier);
8888
Assert.Contains("onnxruntime.dll", verifier);

0 commit comments

Comments
 (0)