using System.Diagnostics; using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using VoiceCat.Crypto; using VoiceCat.Protocol; namespace VoiceCat.Tests; public class TlsInteropTests { [TlsOracleFact] public async Task ManagedClientAndCppServerAgreeOnExporterKeysAndCertificate() { string? oracle = Environment.GetEnvironmentVariable("VOICECAT_TLS_ORACLE"); string directory = Path.Combine(Path.GetTempPath(), "voicecat-tls-" + Guid.NewGuid()); Directory.CreateDirectory(directory); var start = new ProcessStartInfo(oracle!) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true }; start.ArgumentList.Add(directory); using var process = Process.Start(start)!; var error = process.StandardError.ReadToEndAsync(); var stdout = process.StandardOutput.ReadToEndAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); try { int port = 0; while (!int.TryParse(File.Exists(Path.Combine(directory, "port.txt")) ? await File.ReadAllTextAsync(Path.Combine(directory, "port.txt"), timeout.Token) : "", out port)) { Assert.False(process.HasExited, "C++ TLS oracle exited before listening."); await Task.Delay(20, timeout.Token); } using var certificate = X509Certificate2.CreateFromPem(await File.ReadAllTextAsync(Path.Combine(directory, "server.crt"), timeout.Token)); string fingerprint = Convert.ToHexString(SHA256.HashData(certificate.RawData)); using var credentials = ServerCredentials.LoadOrCreate(directory, "existing C++ identity"); Assert.Equal(fingerprint, credentials.CertificateFingerprint); Assert.Equal(32, credentials.Identity.PublicKey.Length); using var client = TlsSession.CreateClient(value => value == fingerprint); using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync("127.0.0.1", port, timeout.Token); byte[] buffer = new byte[16384]; async Task Flush() { while (client.PendingCiphertextBytes > 0) { int count = client.DrainCiphertext(buffer); int sent = 0; while (sent < count) sent += await socket.SendAsync(buffer.AsMemory(sent, count - sent), SocketFlags.None, timeout.Token); } } async Task Receive() { int count = await socket.ReceiveAsync(buffer, SocketFlags.None, timeout.Token); Assert.True(count > 0, "TLS oracle closed unexpectedly."); client.ReceiveCiphertext(buffer.AsSpan(0, count)); } while (!client.IsReady) { await Flush(); await Receive(); } await Flush(); byte[] packet = new byte[41]; int received = 0; while (received < packet.Length) { int count = client.ReadPlaintext(packet.AsSpan(received)); received += count; if (count == 0) { await Flush(); await Receive(); } } using var decryptor = client.CreateMediaDecryptor(); byte[] plaintext = new byte[5]; Assert.True(decryptor.TryDecrypt(packet, plaintext, out var header, out _)); Assert.Equal("hello"u8.ToArray(), plaintext); Assert.Equal(fingerprint, client.PeerCertificateFingerprint); using var encryptor = client.CreateMediaEncryptor(); encryptor.Encrypt(header, plaintext, packet); client.WritePlaintext(packet); await Flush(); byte[] ack = new byte[1]; while (client.ReadPlaintext(ack) == 0) { await Flush(); await Receive(); } Assert.Equal(1, ack[0]); await process.WaitForExitAsync(timeout.Token); Assert.True(process.ExitCode == 0, await error); await stdout; } finally { if (!process.HasExited) { process.Kill(entireProcessTree: true); await process.WaitForExitAsync(); } Directory.Delete(directory, recursive: true); } } } public sealed class TlsOracleFactAttribute : FactAttribute { public TlsOracleFactAttribute() { if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VOICECAT_TLS_ORACLE"))) Skip = "Build the native TLS oracle and set VOICECAT_TLS_ORACLE to its executable path."; } }