Make the Console<->Launcher system source-built and modern now that the console is under our control and the WinXP-era pods are gone. Contract extraction (Contract/Tesla.Contract.csproj): - One multi-targeted (net48;net8.0-windows) source project for the RPC contract, replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib. SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj): - net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll. Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned): - Console<->Launcher RPC is now length-prefixed System.Text.Json frames (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by method name. Deleted the SerializationBinder / MethodInfoProxy machinery. - Console-local BinaryFormatter (Site config, mission replays) intentionally retained: local net48 file I/O, not the network surface. Runtime modernization: - Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are 64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat) with SYSLIB0041 suppressed and documented. Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON round-trip of every request/response shape); removed the now-obsolete BinaryFormatter byte-identity guard. Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a NuGet restore collision between the two Launcher projects sharing one folder. NOT runtime-verified against a live pod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
6.3 KiB
C#
148 lines
6.3 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Security.Cryptography;
|
|
using Xunit;
|
|
|
|
namespace TeslaConsole.DiffTests
|
|
{
|
|
/// <summary>
|
|
/// Proves the source-built secure-config library (SecureConfig/Tesla.SecureConfig,
|
|
/// emitting assembly TeslaSecureConfiguration) is behaviourally identical to the
|
|
/// original vendored TeslaSecureConfiguration.dll for the parts that define the
|
|
/// over-the-wire provisioning format: the OFB keystream cipher and the
|
|
/// BasicConfigResponse byte layout. Byte-identity against the original binary
|
|
/// transitively guarantees compatibility with the pod-side counterpart (the
|
|
/// Launcher's hand-written OFBDuplexStream), which was built against that binary.
|
|
///
|
|
/// The original is loaded out-of-band with Assembly.LoadFile (it shares identity
|
|
/// with the source build); construction logic is identical reflection over both.
|
|
/// </summary>
|
|
public class SecureConfigCompatTests
|
|
{
|
|
private static readonly Assembly SourceCfg = typeof(Tesla.BasicConfigResponse).Assembly;
|
|
private static readonly Assembly VendoredCfg = LoadVendored();
|
|
|
|
private static Assembly LoadVendored()
|
|
{
|
|
string path = Path.Combine(AssemblyPaths.RepoRoot, "lib", "TeslaSecureConfiguration.dll");
|
|
Assert.True(File.Exists(path), "Vendored baseline not found: " + path);
|
|
return Assembly.LoadFile(path);
|
|
}
|
|
|
|
// Fixed, deterministic inputs so both assemblies see identical material.
|
|
private static readonly byte[] Key = BuildBytes(32, i => (byte)(i * 7 + 1));
|
|
private static readonly byte[] Iv = BuildBytes(16, i => (byte)(i * 13 + 5));
|
|
private static readonly byte[] Plaintext =
|
|
System.Text.Encoding.UTF8.GetBytes("The quick brown pod jumps over the lazy console. 0123456789!");
|
|
|
|
[Fact]
|
|
public void Baseline_And_Source_Are_Distinct_Assemblies_With_Same_Identity()
|
|
{
|
|
Assert.NotEqual(SourceCfg.Location, VendoredCfg.Location);
|
|
Assert.Equal(VendoredCfg.GetName().Name, SourceCfg.GetName().Name);
|
|
Assert.Equal(VendoredCfg.GetName().Version, SourceCfg.GetName().Version);
|
|
}
|
|
|
|
[Fact]
|
|
public void OFBCryptoStream_Produces_Identical_Ciphertext()
|
|
{
|
|
byte[] fromVendored = Encrypt(VendoredCfg);
|
|
byte[] fromSource = Encrypt(SourceCfg);
|
|
Assert.Equal(fromVendored, fromSource);
|
|
// Sanity: it actually encrypted (output differs from plaintext).
|
|
Assert.NotEqual(Plaintext, fromSource);
|
|
}
|
|
|
|
[Fact]
|
|
public void OFBCryptoStream_RoundTrips()
|
|
{
|
|
byte[] cipher = Encrypt(SourceCfg);
|
|
byte[] plain = Decrypt(SourceCfg, cipher);
|
|
Assert.Equal(Plaintext, plain);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConfigResponse_ToBytes_Is_Identical()
|
|
{
|
|
Assert.Equal(ConfigToBytes(VendoredCfg), ConfigToBytes(SourceCfg));
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConfigResponse_RoundTrips_Across_Implementations()
|
|
{
|
|
// Bytes produced by the vendored impl must parse correctly in the source impl.
|
|
byte[] bytes = ConfigToBytes(VendoredCfg);
|
|
object parsed = Activator.CreateInstance(
|
|
SourceCfg.GetType("Tesla.BasicConfigResponse", true), bytes);
|
|
Assert.Equal("192.168.1.50", Prop(parsed, "Address").ToString());
|
|
Assert.Equal("255.255.255.0", Prop(parsed, "Mask").ToString());
|
|
Assert.Equal("192.168.1.1", Prop(parsed, "Gateway").ToString());
|
|
Assert.Equal("8.8.8.8", Prop(parsed, "Dns").ToString());
|
|
Assert.Equal("POD-01", Prop(parsed, "HostName"));
|
|
}
|
|
|
|
// ── helpers ──────────────────────────────────────────────────────────
|
|
|
|
private static byte[] Encrypt(Assembly asm)
|
|
{
|
|
using var aes = Rijndael.Create();
|
|
aes.Mode = CipherMode.ECB;
|
|
aes.Key = Key;
|
|
var ms = new MemoryStream();
|
|
var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Write, Iv);
|
|
ofb.Write(Plaintext, 0, Plaintext.Length);
|
|
ofb.Flush();
|
|
return ms.ToArray();
|
|
}
|
|
|
|
private static byte[] Decrypt(Assembly asm, byte[] cipher)
|
|
{
|
|
using var aes = Rijndael.Create();
|
|
aes.Mode = CipherMode.ECB;
|
|
aes.Key = Key;
|
|
var ms = new MemoryStream(cipher);
|
|
var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Read, Iv);
|
|
var outBuf = new byte[Plaintext.Length];
|
|
int off = 0;
|
|
while (off < outBuf.Length)
|
|
{
|
|
int n = ofb.Read(outBuf, off, outBuf.Length - off);
|
|
if (n == 0) break;
|
|
off += n;
|
|
}
|
|
return outBuf;
|
|
}
|
|
|
|
private static Stream NewOfb(Assembly asm, Stream inner, ICryptoTransform t, CryptoStreamMode mode, byte[] iv)
|
|
// OFBCryptoStream keeps the IV array by reference and reuses it as its
|
|
// ping-pong keystream scratch buffer, so it MUST get its own copy — the
|
|
// real callers always pass a freshly generated IV.
|
|
=> (Stream)Activator.CreateInstance(
|
|
asm.GetType("System.Security.Cryptography.OFBCryptoStream", true),
|
|
inner, t, mode, (byte[])iv.Clone());
|
|
|
|
private static byte[] ConfigToBytes(Assembly asm)
|
|
{
|
|
Type t = asm.GetType("Tesla.BasicConfigResponse", true);
|
|
object cfg = Activator.CreateInstance(t,
|
|
IPAddress.Parse("192.168.1.50"),
|
|
IPAddress.Parse("255.255.255.0"),
|
|
IPAddress.Parse("192.168.1.1"),
|
|
IPAddress.Parse("8.8.8.8"),
|
|
"POD-01");
|
|
return (byte[])t.GetMethod("ToBytes").Invoke(cfg, null);
|
|
}
|
|
|
|
private static object Prop(object o, string name) => o.GetType().GetProperty(name).GetValue(o, null);
|
|
|
|
private static byte[] BuildBytes(int n, Func<int, byte> f)
|
|
{
|
|
var b = new byte[n];
|
|
for (int i = 0; i < n; i++) b[i] = f(i);
|
|
return b;
|
|
}
|
|
}
|
|
}
|