using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using VoiceCat.Crypto; using VoiceCat.Protocol; namespace VoiceCat.Tests; public class TlsTests { [Fact] public void ManagedTlsHandshakeExportsMatchingDirectionalKeys() { var (pem, key, fingerprint) = Credentials(); using var server = TlsSession.CreateServer(pem, key); using var client = TlsSession.CreateClient(value => value == fingerprint); Assert.Throws(() => client.CreateMediaEncryptor()); Handshake(client, server); Assert.Equal(fingerprint, client.PeerCertificateFingerprint); Assert.Equal(server.ExportMediaKey(0), client.ExportMediaKey(0)); Assert.Equal(server.ExportMediaKey(1), client.ExportMediaKey(1)); Assert.NotEqual(client.ExportMediaKey(0), client.ExportMediaKey(1)); client.WritePlaintext("hello"u8); Pump(client, server); byte[] output = new byte[5]; Assert.Equal(5, server.ReadPlaintext(output)); Assert.Equal("hello"u8.ToArray(), output); using var encryptor = server.CreateMediaEncryptor(); using var decryptor = client.CreateMediaDecryptor(); byte[] packet = new byte[41]; encryptor.Encrypt(new(MediaFrameType.Voice, 0, 0, 42, 0, 960), "hello"u8, packet); Assert.True(decryptor.TryDecrypt(packet, output, out _, out _)); Assert.Equal("hello"u8.ToArray(), output); } [Fact] public void CertificateRejectionPreventsApplicationDataAndMediaKeys() { var (pem, key, _) = Credentials(); using var server = TlsSession.CreateServer(pem, key); using var client = TlsSession.CreateClient(_ => false); Assert.ThrowsAny(() => Handshake(client, server)); Assert.False(client.IsReady); Assert.Throws(() => client.CreateMediaDecryptor()); Assert.Throws(() => client.WritePlaintext("secret"u8)); } [Fact] public void CloseNotifyEndsSessionAndAbruptEofIsRejected() { var (pem, key, fingerprint) = Credentials(); using var server = TlsSession.CreateServer(pem, key); using var client = TlsSession.CreateClient(value => value == fingerprint); Handshake(client, server); client.Close(); Pump(client, server); Assert.False(client.IsReady); Assert.False(server.IsReady); server.CompleteInput(); using var incomplete = TlsSession.CreateClient(_ => true); Assert.ThrowsAny(() => incomplete.CompleteInput()); } [Fact] public void TlsTwelveCannotNegotiateWithManagedServer() { var (pem, key, _) = Credentials(); using var server = TlsSession.CreateServer(pem, key); var legacy = new Org.BouncyCastle.Tls.TlsClientProtocol(); legacy.Connect(new LegacyPeer()); byte[] hello = new byte[legacy.GetAvailableOutputBytes()]; legacy.ReadOutput(hello, 0, hello.Length); Assert.ThrowsAny(() => server.ReceiveCiphertext(hello)); Assert.False(server.IsReady); Assert.Throws(() => server.CreateMediaEncryptor()); } private sealed class LegacyPeer() : Org.BouncyCastle.Tls.DefaultTlsClient(new Org.BouncyCastle.Tls.Crypto.Impl.BC.BcTlsCrypto()) { protected override Org.BouncyCastle.Tls.ProtocolVersion[] GetSupportedVersions() => [Org.BouncyCastle.Tls.ProtocolVersion.TLSv12]; public override Org.BouncyCastle.Tls.TlsAuthentication GetAuthentication() => throw new InvalidOperationException("TLS 1.2 must be rejected before authentication."); } internal static (string Certificate, string Key, string Fingerprint) Credentials() { using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); var request = new System.Security.Cryptography.X509Certificates.CertificateRequest("CN=VoiceCat TLS test", key, HashAlgorithmName.SHA256); using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1)); return (certificate.ExportCertificatePem(), key.ExportPkcs8PrivateKeyPem(), Convert.ToHexString(SHA256.HashData(certificate.RawData))); } internal static void Handshake(TlsSession client, TlsSession server) { for (int i = 0; i < 100 && (!client.IsReady || !server.IsReady); i++) { Pump(client, server); Pump(server, client); } Assert.True(client.IsReady); Assert.True(server.IsReady); } private static void Pump(TlsSession sender, TlsSession receiver) { byte[] buffer = new byte[17]; while (sender.PendingCiphertextBytes > 0) { int count = sender.DrainCiphertext(buffer); receiver.ReceiveCiphertext(buffer.AsSpan(0, count)); } } }