|
3 | 3 | using System.Net.Http; |
4 | 4 | using System.Net.Sockets; |
5 | 5 | using System.Runtime.InteropServices; |
6 | | -using System.Security.Cryptography; |
7 | | -using System.Security.Cryptography.X509Certificates; |
8 | | -using System.Text; |
9 | 6 | using System.Text.Json; |
10 | 7 | using OpenClaw.Connection; |
11 | 8 | using OpenClaw.Shared; |
@@ -1179,239 +1176,6 @@ private static void ValidateConfValue(Dictionary<string, Dictionary<string, stri |
1179 | 1176 | // GATEWAY INSTALL STEPS |
1180 | 1177 | // ═══════════════════════════════════════════════════════════════════ |
1181 | 1178 |
|
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 | | - |
1415 | 1179 | public sealed class InstallCliStep : SetupStep |
1416 | 1180 | { |
1417 | 1181 | public override string Id => "install-cli"; |
|
0 commit comments