Files
TeslaSuite/Console/tests/TeslaConsole.DiffTests/SecureConfigCompatTests.cs
T
CydandClaude Opus 4.8 b9d8027cf6 Extract shared contract, drop BinaryFormatter wire, modernize to net8/x64
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>
2026-06-30 08:15:17 -05:00

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;
}
}
}