Skip to content

Commit 856c620

Browse files
authored
Back out WSL CA sync setup step
Remove the broken Windows CA certificate sync setup step after validation showed it was dead on arrival.
1 parent 7ad7fca commit 856c620

4 files changed

Lines changed: 3 additions & 343 deletions

File tree

‎src/OpenClaw.SetupEngine/SetupPipeline.cs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public static List<SetupStep> BuildDefaultSteps()
5656
new CreateWslInstanceStep(),
5757
new ConfigureWslInstanceStep(),
5858
new ValidateWslLockdownStep(),
59-
new SyncWindowsCaCertsStep(),
6059
new InstallCliStep(),
6160
new ConfigureGatewayStep(),
6261
new InstallGatewayServiceStep(),

‎src/OpenClaw.SetupEngine/SetupSteps.cs‎

Lines changed: 0 additions & 236 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
using System.Net.Http;
44
using System.Net.Sockets;
55
using System.Runtime.InteropServices;
6-
using System.Security.Cryptography;
7-
using System.Security.Cryptography.X509Certificates;
8-
using System.Text;
96
using System.Text.Json;
107
using OpenClaw.Connection;
118
using OpenClaw.Shared;
@@ -1179,239 +1176,6 @@ private static void ValidateConfValue(Dictionary<string, Dictionary<string, stri
11791176
// GATEWAY INSTALL STEPS
11801177
// ═══════════════════════════════════════════════════════════════════
11811178

1182-
/// <summary>
1183-
/// Exports Windows trusted root CA certificates into the WSL
1184-
/// distro's system trust store. This resolves curl exit-60 failures that
1185-
/// occur on corporate networks where a TLS-intercepting proxy injects a
1186-
/// self-signed certificate that the WSL Ubuntu instance does not trust.
1187-
/// The step is non-fatal: if the Windows store cannot be read or the WSL
1188-
/// trust store cannot be updated, it logs a warning and continues so that
1189-
/// setups on non-proxied networks are not blocked.
1190-
/// </summary>
1191-
public sealed class SyncWindowsCaCertsStep : SetupStep
1192-
{
1193-
private const string ManagedCertDirectory = "/usr/local/share/ca-certificates/openclaw-windows";
1194-
private const string InstallScript = """
1195-
set -euo pipefail
1196-
1197-
target=/usr/local/share/ca-certificates/openclaw-windows
1198-
parent=/usr/local/share/ca-certificates
1199-
backup_parent=/var/lib/openclaw-setup
1200-
backup="$backup_parent/windows-ca-certificates.backup"
1201-
install -d -m 0700 -o root -g root "$backup_parent"
1202-
staging="$(mktemp -d "$parent/.openclaw-windows.XXXXXX")"
1203-
chmod 0755 "$staging"
1204-
old_moved=0
1205-
new_installed=0
1206-
1207-
cleanup() {
1208-
rm -rf -- "$staging"
1209-
if [ "$new_installed" -eq 1 ]; then
1210-
rm -rf -- "$target"
1211-
fi
1212-
if [ "$old_moved" -eq 1 ] && [ -d "$backup" ]; then
1213-
mv -- "$backup" "$target"
1214-
fi
1215-
rm -rf -- "$backup"
1216-
if [ "$old_moved" -eq 1 ] || [ "$new_installed" -eq 1 ]; then
1217-
update-ca-certificates --fresh >/dev/null 2>&1 || true
1218-
fi
1219-
}
1220-
trap cleanup EXIT
1221-
1222-
if [ -d "$backup" ]; then
1223-
rm -rf -- "$target"
1224-
mv -- "$backup" "$target"
1225-
update-ca-certificates --fresh
1226-
fi
1227-
1228-
count=0
1229-
while IFS=$'\t' read -r fingerprint payload; do
1230-
if [ -z "$fingerprint" ] && [ -z "$payload" ]; then
1231-
continue
1232-
fi
1233-
case "$fingerprint" in
1234-
''|*[!0-9A-Fa-f]*) echo "Invalid certificate fingerprint" >&2; exit 64 ;;
1235-
esac
1236-
if [ "${#fingerprint}" -ne 64 ]; then
1237-
echo "Invalid certificate fingerprint length" >&2
1238-
exit 64
1239-
fi
1240-
case "$payload" in
1241-
''|*[!A-Za-z0-9+/=]*) echo "Invalid certificate payload" >&2; exit 64 ;;
1242-
esac
1243-
1244-
fingerprint="${fingerprint,,}"
1245-
cert_path="$staging/$fingerprint.crt"
1246-
{
1247-
printf '%s\n' '-----BEGIN CERTIFICATE-----'
1248-
printf '%s\n' "$payload" | fold -w 64
1249-
printf '%s\n' '-----END CERTIFICATE-----'
1250-
} > "$cert_path"
1251-
chmod 0644 "$cert_path"
1252-
1253-
if ! openssl x509 -in "$cert_path" -noout >/dev/null 2>&1; then
1254-
echo "Invalid X.509 certificate for $fingerprint" >&2
1255-
exit 65
1256-
fi
1257-
actual="$(openssl x509 -in "$cert_path" -outform DER | sha256sum | cut -d ' ' -f1)"
1258-
if [ "$actual" != "$fingerprint" ]; then
1259-
echo "Certificate fingerprint mismatch for $fingerprint" >&2
1260-
exit 65
1261-
fi
1262-
count=$((count + 1))
1263-
done
1264-
1265-
if [ "$count" -eq 0 ]; then
1266-
echo "No certificates received" >&2
1267-
exit 64
1268-
fi
1269-
1270-
if [ -d "$target" ]; then
1271-
mv -- "$target" "$backup"
1272-
old_moved=1
1273-
fi
1274-
mv -- "$staging" "$target"
1275-
new_installed=1
1276-
update-ca-certificates --fresh
1277-
1278-
rm -rf -- "$backup"
1279-
old_moved=0
1280-
new_installed=0
1281-
trap - EXIT
1282-
""";
1283-
private const string RollbackScript = """
1284-
set -e
1285-
target=/usr/local/share/ca-certificates/openclaw-windows
1286-
backup=/var/lib/openclaw-setup/windows-ca-certificates.backup
1287-
if [ -d "$target" ]; then
1288-
rm -rf -- "$target"
1289-
update-ca-certificates --fresh
1290-
fi
1291-
rm -rf -- "$backup"
1292-
""";
1293-
1294-
private readonly Func<WindowsCaCertificateExport> _exportCertificates;
1295-
1296-
public SyncWindowsCaCertsStep() : this(ExportWindowsTrustedRoots) { }
1297-
1298-
internal SyncWindowsCaCertsStep(Func<WindowsCaCertificateExport> exportCertificates)
1299-
=> _exportCertificates = exportCertificates;
1300-
1301-
public override string Id => "sync-ca-certs";
1302-
public override string DisplayName => "Sync Windows CA certificates to WSL";
1303-
public override bool CanRetry => false;
1304-
1305-
public override async Task<StepResult> ExecuteAsync(SetupContext ctx, CancellationToken ct)
1306-
{
1307-
var distro = ctx.DistroName!;
1308-
1309-
WindowsCaCertificateExport export;
1310-
try
1311-
{
1312-
export = _exportCertificates();
1313-
}
1314-
catch (Exception ex)
1315-
{
1316-
ctx.Logger.Warn($"Could not read Windows CA store: {ex.Message} — skipping CA sync");
1317-
return StepResult.Ok("Skipped: could not read Windows CA store");
1318-
}
1319-
1320-
foreach (var warning in export.Warnings)
1321-
ctx.Logger.Warn(warning);
1322-
1323-
if (export.CertificateCount == 0)
1324-
{
1325-
ctx.Logger.Warn("Windows CA store returned no certificates — skipping CA sync");
1326-
return StepResult.Ok("Skipped: Windows CA store is empty");
1327-
}
1328-
1329-
var result = await ctx.Commands.RunAsync(
1330-
WslConstants.WslExePath,
1331-
["-d", distro, "-u", "root", "--", "bash", "-c", NormalizeShellScript(InstallScript)],
1332-
TimeSpan.FromSeconds(60),
1333-
workingDirectory: WslConstants.SafeWindowsWorkingDirectory,
1334-
stdinInput: export.Manifest,
1335-
ct: ct);
1336-
1337-
if (result.ExitCode != 0)
1338-
{
1339-
ctx.Logger.Warn($"update-ca-certificates exited {result.ExitCode}: {result.Stderr.Trim()}");
1340-
return StepResult.Ok($"Skipped: could not update WSL CA certificates (exit {result.ExitCode})");
1341-
}
1342-
1343-
ctx.Logger.Info($"Synced {export.CertificateCount} Windows CA certificates to WSL trust store");
1344-
return StepResult.Ok($"Synced {export.CertificateCount} Windows CA certificates to WSL");
1345-
}
1346-
1347-
public override async Task RollbackAsync(SetupContext ctx, CancellationToken ct)
1348-
{
1349-
var distro = ctx.DistroName!;
1350-
var result = await ctx.Commands.RunAsync(
1351-
WslConstants.WslExePath,
1352-
["-d", distro, "-u", "root", "--", "bash", "-c", NormalizeShellScript(RollbackScript)],
1353-
TimeSpan.FromSeconds(30),
1354-
workingDirectory: WslConstants.SafeWindowsWorkingDirectory,
1355-
ct: ct);
1356-
if (result.ExitCode == 0)
1357-
{
1358-
ctx.Logger.Info($"[Rollback] Removed {ManagedCertDirectory} from WSL trust store");
1359-
return;
1360-
}
1361-
1362-
throw new InvalidOperationException(
1363-
$"Could not remove synced CA certificates (exit {result.ExitCode}): {result.Stderr.Trim()}");
1364-
}
1365-
1366-
internal static WindowsCaCertificateExport BuildCertificateManifest(
1367-
IEnumerable<byte[]> rawCertificates,
1368-
IReadOnlyList<string>? warnings = null)
1369-
{
1370-
var certificates = new SortedDictionary<string, string>(StringComparer.Ordinal);
1371-
foreach (var rawCertificate in rawCertificates)
1372-
{
1373-
var fingerprint = Convert.ToHexString(SHA256.HashData(rawCertificate)).ToLowerInvariant();
1374-
certificates.TryAdd(fingerprint, Convert.ToBase64String(rawCertificate));
1375-
}
1376-
1377-
var manifest = new StringBuilder();
1378-
foreach (var (fingerprint, payload) in certificates)
1379-
manifest.Append(fingerprint).Append('\t').Append(payload).Append('\n');
1380-
1381-
return new WindowsCaCertificateExport(manifest.ToString(), certificates.Count, warnings ?? []);
1382-
}
1383-
1384-
private static string NormalizeShellScript(string script)
1385-
=> script.Replace("\r", "", StringComparison.Ordinal);
1386-
1387-
private static WindowsCaCertificateExport ExportWindowsTrustedRoots()
1388-
{
1389-
var rawCertificates = new List<byte[]>();
1390-
var warnings = new List<string>();
1391-
foreach (var location in new[] { StoreLocation.LocalMachine, StoreLocation.CurrentUser })
1392-
{
1393-
try
1394-
{
1395-
using var store = new X509Store(StoreName.Root, location);
1396-
store.Open(OpenFlags.ReadOnly);
1397-
foreach (var certificate in store.Certificates)
1398-
rawCertificates.Add(certificate.RawData);
1399-
}
1400-
catch (Exception ex) when (ex is CryptographicException or UnauthorizedAccessException)
1401-
{
1402-
warnings.Add($"Could not read Windows {location} Root store: {ex.Message}");
1403-
}
1404-
}
1405-
1406-
return BuildCertificateManifest(rawCertificates, warnings);
1407-
}
1408-
}
1409-
1410-
internal sealed record WindowsCaCertificateExport(
1411-
string Manifest,
1412-
int CertificateCount,
1413-
IReadOnlyList<string> Warnings);
1414-
14151179
public sealed class InstallCliStep : SetupStep
14161180
{
14171181
public override string Id => "install-cli";

‎tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@ public void BuildDefaultSteps_IncludesCurrentSetupFlow()
6060
{
6161
var steps = SetupStepFactory.BuildDefaultSteps();
6262

63-
Assert.Equal(20, steps.Count);
63+
Assert.Equal(19, steps.Count);
6464
Assert.IsType<PreflightOsStep>(steps[0]);
6565
Assert.IsType<PreflightWslStep>(steps[1]);
6666
Assert.IsType<CleanupStaleDistroStep>(steps[2]);
6767
Assert.IsType<CleanupStaleGatewayStep>(steps[3]);
6868
Assert.Contains(steps, s => s is ValidateWslLockdownStep);
69-
var caSyncIndex = steps.FindIndex(s => s is SyncWindowsCaCertsStep);
69+
var lockdownIndex = steps.FindIndex(s => s is ValidateWslLockdownStep);
7070
var cliInstallIndex = steps.FindIndex(s => s is InstallCliStep);
71-
Assert.Equal(cliInstallIndex - 1, caSyncIndex);
71+
Assert.Equal(lockdownIndex + 1, cliInstallIndex);
7272
Assert.Contains(steps, s => s is RunGatewayWizardStep);
7373
var pairNodeIndex = steps.FindIndex(s => s is PairNodeStep);
7474
Assert.IsType<VerifyEndToEndStep>(steps[pairNodeIndex + 1]);

‎tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs‎

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,109 +1521,6 @@ public async Task PairOperatorStep_FailsWhenGatewayNotReachableFromWindows()
15211521
Assert.Contains("not reachable", result.Message, StringComparison.OrdinalIgnoreCase);
15221522
}
15231523

1524-
// ─── SyncWindowsCaCertsStep ───
1525-
1526-
[Fact]
1527-
public void SyncWindowsCaCerts_BuildCertificateManifest_DeduplicatesAndSortsCertificates()
1528-
{
1529-
byte[] first = [1, 2, 3];
1530-
byte[] second = [4, 5, 6];
1531-
1532-
var export = SyncWindowsCaCertsStep.BuildCertificateManifest([second, first, second]);
1533-
var lines = export.Manifest.Split('\n', StringSplitOptions.RemoveEmptyEntries);
1534-
1535-
Assert.Equal(2, export.CertificateCount);
1536-
Assert.Equal(2, lines.Length);
1537-
Assert.Equal(lines.Order(StringComparer.Ordinal), lines);
1538-
Assert.All(lines, line => Assert.Equal(2, line.Split('\t').Length));
1539-
}
1540-
1541-
[Fact]
1542-
public async Task SyncWindowsCaCerts_InstallsIndividualCertificatesAsWslRoot()
1543-
{
1544-
var export = SyncWindowsCaCertsStep.BuildCertificateManifest([[1, 2, 3]]);
1545-
var commands = new FakeCommandRunner(_ => Ok());
1546-
var ctx = CreateContext(commands: commands);
1547-
ctx.DistroName = "test-distro";
1548-
1549-
var result = await new SyncWindowsCaCertsStep(() => export)
1550-
.ExecuteAsync(ctx, CancellationToken.None);
1551-
1552-
Assert.True(result.IsSuccess);
1553-
var call = Assert.Single(commands.DetailedCalls);
1554-
Assert.Equal(WslConstants.WslExePath, call.Executable);
1555-
Assert.Equal(["-d", "test-distro", "-u", "root", "--", "bash", "-c"], call.Arguments[..7]);
1556-
Assert.Contains("openssl x509", call.Arguments[7], StringComparison.Ordinal);
1557-
Assert.Contains("backup_parent=/var/lib/openclaw-setup", call.Arguments[7], StringComparison.Ordinal);
1558-
Assert.Contains("chmod 0755 \"$staging\"", call.Arguments[7], StringComparison.Ordinal);
1559-
Assert.Contains("chmod 0644 \"$cert_path\"", call.Arguments[7], StringComparison.Ordinal);
1560-
Assert.Contains("update-ca-certificates --fresh", call.Arguments[7], StringComparison.Ordinal);
1561-
Assert.Contains("$fingerprint.crt", call.Arguments[7], StringComparison.Ordinal);
1562-
Assert.DoesNotContain('\r', call.Arguments[7]);
1563-
Assert.Equal(export.Manifest, call.StdinInput);
1564-
}
1565-
1566-
[Fact]
1567-
public async Task SyncWindowsCaCerts_FailureIsNonFatal()
1568-
{
1569-
var export = SyncWindowsCaCertsStep.BuildCertificateManifest([[1, 2, 3]]);
1570-
var commands = new FakeCommandRunner(_ => Fail("update failed"));
1571-
var ctx = CreateContext(commands: commands);
1572-
ctx.DistroName = "test-distro";
1573-
1574-
var result = await new SyncWindowsCaCertsStep(() => export)
1575-
.ExecuteAsync(ctx, CancellationToken.None);
1576-
1577-
Assert.True(result.IsSuccess);
1578-
Assert.Contains("Skipped", result.Message, StringComparison.Ordinal);
1579-
}
1580-
1581-
[Fact]
1582-
public async Task SyncWindowsCaCerts_EmptyStoreSkipsWslCall()
1583-
{
1584-
var commands = new FakeCommandRunner(_ => Ok());
1585-
var ctx = CreateContext(commands: commands);
1586-
ctx.DistroName = "test-distro";
1587-
var empty = new WindowsCaCertificateExport("", 0, []);
1588-
1589-
var result = await new SyncWindowsCaCertsStep(() => empty)
1590-
.ExecuteAsync(ctx, CancellationToken.None);
1591-
1592-
Assert.True(result.IsSuccess);
1593-
Assert.Empty(commands.DetailedCalls);
1594-
}
1595-
1596-
[Fact]
1597-
public async Task SyncWindowsCaCerts_RollbackRemovesManagedDirectoryAndRebuildsTrust()
1598-
{
1599-
var commands = new FakeCommandRunner(_ => Ok());
1600-
var ctx = CreateContext(commands: commands);
1601-
ctx.DistroName = "test-distro";
1602-
1603-
await new SyncWindowsCaCertsStep(() => new WindowsCaCertificateExport("", 0, []))
1604-
.RollbackAsync(ctx, CancellationToken.None);
1605-
1606-
var call = Assert.Single(commands.DetailedCalls);
1607-
Assert.Equal(["-d", "test-distro", "-u", "root", "--", "bash", "-c"], call.Arguments[..7]);
1608-
Assert.Contains("rm -rf -- \"$target\"", call.Arguments[7], StringComparison.Ordinal);
1609-
Assert.Contains("update-ca-certificates --fresh", call.Arguments[7], StringComparison.Ordinal);
1610-
Assert.DoesNotContain('\r', call.Arguments[7]);
1611-
}
1612-
1613-
[Fact]
1614-
public async Task SyncWindowsCaCerts_RollbackFailureIsPropagated()
1615-
{
1616-
var commands = new FakeCommandRunner(_ => Fail("remove failed"));
1617-
var ctx = CreateContext(commands: commands);
1618-
ctx.DistroName = "test-distro";
1619-
1620-
var error = await Assert.ThrowsAsync<InvalidOperationException>(() =>
1621-
new SyncWindowsCaCertsStep(() => new WindowsCaCertificateExport("", 0, []))
1622-
.RollbackAsync(ctx, CancellationToken.None));
1623-
1624-
Assert.Contains("remove failed", error.Message, StringComparison.Ordinal);
1625-
}
1626-
16271524
[Fact]
16281525
public void WindowsNodeContext_CanSkipWhenDisabled()
16291526
{

0 commit comments

Comments
 (0)