diff --git a/.gitignore b/.gitignore index 2f5890b..9679359 100644 --- a/.gitignore +++ b/.gitignore @@ -256,4 +256,4 @@ paket-files/ .sonarlint # Auto-generated test configuration (produced from .runsettings at build time) -Tests/SocketTests/TestConfiguration.cs +Tests/**/TestConfiguration.cs diff --git a/.runsettings b/.runsettings index 0acfbb0..b3dca5d 100644 --- a/.runsettings +++ b/.runsettings @@ -3,7 +3,7 @@ .\TestResults - 60000 + 90000 net48 x64 diff --git a/Tests/IPAddressTests/IPAddressTests.nfproj b/Tests/IPAddressTests/IPAddressTests.nfproj index 0597652..ea2a2ec 100644 --- a/Tests/IPAddressTests/IPAddressTests.nfproj +++ b/Tests/IPAddressTests/IPAddressTests.nfproj @@ -62,4 +62,4 @@ - + \ No newline at end of file diff --git a/Tests/NetworkHelperTests/NetworkHelperTests.nfproj b/Tests/NetworkHelperTests/NetworkHelperTests.nfproj index d34f9f1..8da3f45 100644 --- a/Tests/NetworkHelperTests/NetworkHelperTests.nfproj +++ b/Tests/NetworkHelperTests/NetworkHelperTests.nfproj @@ -71,4 +71,4 @@ - + \ No newline at end of file diff --git a/Tests/NetworkTestCompanion/CommandServer.cs b/Tests/NetworkTestCompanion/CommandServer.cs index 9c06674..e4ce842 100644 --- a/Tests/NetworkTestCompanion/CommandServer.cs +++ b/Tests/NetworkTestCompanion/CommandServer.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Net; +using System.Net.Security; using System.Net.Sockets; +using System.Security.Authentication; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -20,6 +22,9 @@ namespace NetworkTestCompanion; /// { "cmd": "stop", "port": N } /// { "cmd": "stop_all" } /// { "cmd": "connect_to", "host": "...", "port": N } +/// { "cmd": "start_tls_echo", "port": N } +/// { "cmd": "tls_connect_to", "host": "...", "port": N } +/// { "cmd": "tls_connect_echo", "host": "...", "port": N, "data": "base64..." } /// internal sealed class CommandServer : IDisposable { @@ -112,6 +117,9 @@ private string ProcessCommand(string json) "stop" => Stop(node), "stop_all" => StopAll(), "connect_to" => ConnectTo(node), + "start_tls_echo" => StartTlsEcho(node), + "tls_connect_to" => TlsConnectTo(node), + "tls_connect_echo" => TlsConnectEcho(node), _ => Error($"unknown command: {cmd}") }; } @@ -237,6 +245,179 @@ private string ConnectTo(JsonNode node) } } + private string StartTlsEcho(JsonNode node) + { + if (!TryGetPort(node, out var port, out var err)) return err!; + + lock (_lock) + { + // Replace any stale server left registered on this port by a previous + // (possibly aborted) test run — the companion is long-lived across runs. + if (_activeServers.TryGetValue(port, out var existing)) + { + existing.Dispose(); + _activeServers.Remove(port); + Console.WriteLine($"[CMD] Replaced stale server on port {port}"); + } + + var server = new TlsEchoServer(_bindAddress, port, TestCertificates.ServerCert); + try + { + server.Start(); + } + catch (Exception ex) + { + server.Dispose(); + return Error(ex.Message); + } + + _activeServers[port] = server; + } + + Console.WriteLine($"[CMD] TLS echo started on port {port}"); + return Ok(); + } + + private string TlsConnectTo(JsonNode node) + { + var host = node["host"]?.GetValue(); + if (string.IsNullOrEmpty(host)) return Error("missing 'host'"); + if (!TryGetPort(node, out var port, out var err)) return err!; + + TcpClient? connectClient = null; + try + { + connectClient = new TcpClient(); + if (!connectClient.ConnectAsync(host, port).Wait(TimeSpan.FromSeconds(5))) + { + connectClient.Dispose(); + return Error($"connect to {host}:{port} timed out"); + } + + Console.WriteLine($"[CMD] tls_connect_to {host}:{port} TCP connected, starting TLS handshake in background"); + + // TLS handshake + keep-alive runs in background so the device + // can call Accept() and AuthenticateAsServer() after getting Ok. + var clientForBg = connectClient; + _ = Task.Run(async () => + { + try + { + var sslStream = new SslStream( + clientForBg.GetStream(), + leaveInnerStreamOpen: false, + (_, _, _, _) => true); + + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = host, + EnabledSslProtocols = SslProtocols.Tls12 + }); + + Console.WriteLine($"[CMD] tls_connect_to {host}:{port} TLS handshake succeeded"); + + await Task.Delay(2000); + sslStream.Dispose(); + clientForBg.Dispose(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[CMD] tls_connect_to {host}:{port} TLS failed: {ex.Message}"); + clientForBg.Dispose(); + } + }); + + return Ok(); + } + catch (Exception ex) + { + connectClient?.Dispose(); + Console.Error.WriteLine($"[CMD] tls_connect_to {host}:{port} failed: {ex.Message}"); + return Error(ex.Message); + } + } + + private string TlsConnectEcho(JsonNode node) + { + var host = node["host"]?.GetValue(); + if (string.IsNullOrEmpty(host)) return Error("missing 'host'"); + if (!TryGetPort(node, out var port, out var err)) return err!; + var dataB64 = node["data"]?.GetValue(); + if (string.IsNullOrEmpty(dataB64)) return Error("missing 'data'"); + + byte[] dataToSend; + try { dataToSend = Convert.FromBase64String(dataB64); } + catch { return Error("'data' is not valid base64"); } + + TcpClient? connectClient = null; + try + { + connectClient = new TcpClient(); + if (!connectClient.ConnectAsync(host, port).Wait(TimeSpan.FromSeconds(5))) + { + connectClient.Dispose(); + return Error($"connect to {host}:{port} timed out"); + } + + Console.WriteLine($"[CMD] tls_connect_echo {host}:{port} TCP connected, starting TLS + echo in background"); + + // TLS handshake + echo runs in background so the device + // can call Accept() and AuthenticateAsServer() after getting Ok. + var clientForBg = connectClient; + _ = Task.Run(async () => + { + try + { + var sslStream = new SslStream( + clientForBg.GetStream(), + leaveInnerStreamOpen: false, + (_, _, _, _) => true); + + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = host, + EnabledSslProtocols = SslProtocols.Tls12 + }); + + await sslStream.WriteAsync(dataToSend); + await sslStream.FlushAsync(); + + Console.WriteLine($"[CMD] tls_connect_echo {host}:{port}: sent {dataToSend.Length} bytes, waiting for echo"); + + var buf = new byte[4096]; + int totalRead = 0; + using var ms = new MemoryStream(); + + while (totalRead < dataToSend.Length) + { + int read = await sslStream.ReadAsync(buf); + if (read == 0) break; + ms.Write(buf, 0, read); + totalRead += read; + } + + Console.WriteLine($"[CMD] tls_connect_echo {host}:{port}: received {totalRead} bytes echo"); + + sslStream.Dispose(); + clientForBg.Dispose(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[CMD] tls_connect_echo {host}:{port} TLS/echo failed: {ex.Message}"); + clientForBg.Dispose(); + } + }); + + return Ok(); + } + catch (Exception ex) + { + connectClient?.Dispose(); + Console.Error.WriteLine($"[CMD] tls_connect_echo {host}:{port} failed: {ex.Message}"); + return Error(ex.Message); + } + } + private static bool TryGetPort(JsonNode node, out int port, out string? error) { port = 0; diff --git a/Tests/NetworkTestCompanion/Program.cs b/Tests/NetworkTestCompanion/Program.cs index fc93d72..bc56343 100644 --- a/Tests/NetworkTestCompanion/Program.cs +++ b/Tests/NetworkTestCompanion/Program.cs @@ -7,7 +7,7 @@ using NetworkTestCompanion; const int DefaultControlPort = 11000; -int[] WellKnownTcpPorts = [DefaultControlPort, 7, 8, 9, 10, 80, 8080]; +int[] WellKnownTcpPorts = [DefaultControlPort, 7, 8, 9, 10, 80, 8080, 7010, 7011, 7012, 7013, 7014, 7015]; int[] WellKnownUdpPorts = [7, 8, 9]; // Argument parsing diff --git a/Tests/NetworkTestCompanion/TestCertificates.cs b/Tests/NetworkTestCompanion/TestCertificates.cs new file mode 100644 index 0000000..16a35b3 --- /dev/null +++ b/Tests/NetworkTestCompanion/TestCertificates.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Security.Cryptography.X509Certificates; + +namespace NetworkTestCompanion; + +/// +/// Provides the static test certificate used by the companion's TLS echo server. +/// +/// This is a self-signed RSA-2048 test certificate (CN=nanoFramework Test Server). +/// The device connects to the companion's TLS echo server with certificate +/// verification disabled, so the certificate only needs to be valid enough for +/// the Windows SChannel server side to complete a handshake. +/// +/// The SAME certificate and key are embedded on the device side (SslServerTests.cs) +/// for the reverse scenario (device acting as TLS server). Not a secret — a +/// throw-away test certificate. +/// +internal static class TestCertificates +{ + private static readonly Lazy _serverCert = new(LoadServerCertificate); + + internal static X509Certificate2 ServerCert => _serverCert.Value; + + private static X509Certificate2 LoadServerCertificate() + { + // Load cert + key from PEM, then re-import via PFX so Windows SChannel + // can use the private key (SChannel rejects the ephemeral key handle + // that CreateFromPem produces). + using var fromPem = X509Certificate2.CreateFromPem(ServerCertPem, ServerKeyPem); + var pfxBytes = fromPem.Export(X509ContentType.Pfx); + var cert = X509CertificateLoader.LoadPkcs12(pfxBytes, null, X509KeyStorageFlags.Exportable); + + Console.WriteLine("[CERTS] Loaded static test server certificate"); + return cert; + } + + // Self-signed RSA-2048 test certificate, CN=nanoFramework Test Server. + // Valid 2026..2036. Throw-away test cert — not a secret. + internal const string ServerCertPem = +@"-----BEGIN CERTIFICATE----- +MIIDRTCCAi2gAwIBAgIUepRBLWtpFLvLv6rjIIUQxmkVkYkwDQYJKoZIhvcNAQEL +BQAwJDEiMCAGA1UEAwwZbmFub0ZyYW1ld29yayBUZXN0IFNlcnZlcjAeFw0yNjA3 +MTYxMjA0MDJaFw0zNjA3MTMxMjA0MDJaMCQxIjAgBgNVBAMMGW5hbm9GcmFtZXdv +cmsgVGVzdCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu ++yM+X9ZcaawdwfhpJiWa4qlrA/1aV0CoENchMP6XOr4Eq7h/Y8jH+QlKdG2hFe31 +wULiwLJq6QwTQ23a7vRFBgTZCZJSs5QY54o2r7O6pO37Y1w+/d0/4blFLNWd0PQq +Mm8TUKdK3J11dv+n/oY9++4vFHR6Bo3xjHFBvm03vcKETeF3UIX+g6J84lfNmdPs +A3UIFqkWXioC7a2+afnRczAHrrS0Py2KcSv+G5E94ZYQHs0VljY8CpOEV2maxh9S +Bjocv4o6HUejKoWvbXqkftuxztjYx77p++jhICpnNZjpNOb27rJhtGw3HPwtn8IY +I3jIZS72insBEQgKSxBhAgMBAAGjbzBtMB0GA1UdDgQWBBR7BiZTDR7gx4gl2fV1 +Y9fjrb1mMjAfBgNVHSMEGDAWgBR7BiZTDR7gx4gl2fV1Y9fjrb1mMjAJBgNVHRME +AjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEAIeCjTx10sJiacjzajLUJ/dfgSq8VfMUhHQLxG5VF3bPMVbr8LaF+ +sM77OKXOBKigDj65urMUwAoLKXF7UcewvVV239Og1p97acetDDiM3Q72duH8MFAF +K2V9qR6Cj8/jvQdSHWiJVOHE0u31PG6Xwa6aIwXA79VKbJaucvj9hXYz0O3HXTRc +RmvHemTge0p0VhuTL+wNv46mftEZhoSsPZa4S08nv5VX3EyWfQM6eX3ghnq6CsE+ +MQw94r5CV1kZDA/R9IdXh4aRIVCyN0ZmMvfNNrmIJRdr/eLzQc/6DBeh5Wrg7Sc/ +9oUAfyIiPqXG3sW0Txof2L5qbTmallrooQ== +-----END CERTIFICATE-----"; + + // PKCS#1 (traditional RSA) encoding — kept identical to the device-side key + // (SslServerTests.cs), which requires this format for mbedTLS SecureServerInit. + internal const string ServerKeyPem = +@"-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArvsjPl/WXGmsHcH4aSYlmuKpawP9WldAqBDXITD+lzq+BKu4 +f2PIx/kJSnRtoRXt9cFC4sCyaukME0Nt2u70RQYE2QmSUrOUGOeKNq+zuqTt+2Nc +Pv3dP+G5RSzVndD0KjJvE1CnStyddXb/p/6GPfvuLxR0egaN8YxxQb5tN73ChE3h +d1CF/oOifOJXzZnT7AN1CBapFl4qAu2tvmn50XMwB660tD8tinEr/huRPeGWEB7N +FZY2PAqThFdpmsYfUgY6HL+KOh1HoyqFr216pH7bsc7Y2Me+6fvo4SAqZzWY6TTm +9u6yYbRsNxz8LZ/CGCN4yGUu9op7AREICksQYQIDAQABAoIBAB8L2Bj9EB+dcDhn +bhfZ+NoeVUjzkEQzLvmi40i0VLeoaIaToUyY+8rfWNKpDbqDFZGBFMj+v6lQaCAS +2q75rsWAZ+PKWvfpfOFeU5uYWR9InCD6ZCeZC2SGPEUVy2EQ7gF+qU6YBNa3hgiN +cJbyBgeBZ6Vaz7/G4fB1prKvgtlcunjtXAwdme9nkHR2kuG+pGGtNs/qc71bQeOt +5gphHdls+lHX+D6QD/gB2biR1bSJW+Cegz0zNM0nUUz6cA3K9dUTMeyGyI4c8FNt +u/FuRZlri/I7yUAPngVnmq46rJg1Ih0OvXI+jIEHp7SI0e9MiRLnZhB4x3/76940 +qAJ+nnECgYEA5692QqZ0eovLlqlWSrRNP0mG31HxCTrWYvfvfWkfhGPskdsJoOeQ +RXk5Mvfp84miIezv8aXxmQEquxQiP8HRtzQ0TrsvKdlM9XOGwUJULyqtFfSs6vmj +HTD16GGPxWqEy0xAcaQcM31pb8YwuZ5MCNRdhuL4ladWBfMR0z75GssCgYEAwVg8 +hPsX+H4LzCfu32MQCMh+sAR+1xPTLSG3ydJFbx6PE9pX408ZJKc9CtkG8Xz29+y1 +EZnymB1IUpNZxms/4pybyFaKXUa20SpPgSoIrBaL/wdgM4h4Pvs2FSceYN9qY7Z5 +d4DhJEiuez+CAFVqQnrNaLJI7xD094SEv91nQAMCgYA+rq8dOzG6UgYj3e61yXA4 +1ijCVMYUzDFil1fZI07en7ZKg+tn+B6FXVXHX2GRfUQ7T4Jfa5kg3zrzYHAftc2K +dnpMbsJE3UDAC6CCuvJRzIcFsKvz6tRhunRdib+/FqGU6y1oUZE7sQuMrR9TqOtD +XEltjAzbWGmitG+3Kot03wKBgQCunriaCgWOQpj5HB/b1aaHqDzzUDwWmCskGc3a +E3TudRUYAx1ZiPjWZ8zz3SsuM4UCSeEHMpkt1VSab8anM/oQ+wyflbmFoPZAVwxT +RdlrQznRbaHvKRQhHdWsqRYAvAdkY0u1KMsucA5V9fe9wWck/7BBHLROZmw4mJEk +kBxObQKBgQCCrcUNFhm3dxCdi+VgMwrhMOqiO5XYAGw4raQ/BbXwpb1PLbj4xtSZ +WVb3utTBP0WPhf58EcHc8ko4B+1xCMR4B9rntQACfngbUN4wETQ1Gz+bNgaHQyJo +gHYA38gnEOJurr2VLZFaqLgwj+7kpTRL2a0ZTDz8pCxlwDpsi4n1YA== +-----END RSA PRIVATE KEY-----"; +} diff --git a/Tests/NetworkTestCompanion/TlsEchoServer.cs b/Tests/NetworkTestCompanion/TlsEchoServer.cs new file mode 100644 index 0000000..fc27aa5 --- /dev/null +++ b/Tests/NetworkTestCompanion/TlsEchoServer.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace NetworkTestCompanion; + +/// +/// Listens on a TCP port, wraps each accepted connection in an SslStream, +/// and echoes all received bytes back to the sender. Used by the device's +/// "device as TLS client" tests, where the device connects with certificate +/// verification disabled. +/// +internal sealed class TlsEchoServer : IDisposable +{ + private readonly TcpListener _listener; + private readonly X509Certificate2 _serverCert; + private readonly CancellationTokenSource _cts = new(); + private Task? _acceptLoop; + + internal int Port { get; } + + internal TlsEchoServer(IPAddress bindAddress, int port, X509Certificate2 serverCert) + { + Port = port; + _serverCert = serverCert; + _listener = new TcpListener(bindAddress, port); + } + + internal void Start() + { + _listener.Start(); + _acceptLoop = AcceptLoopAsync(_cts.Token); + } + + private async Task AcceptLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + TcpClient client; + try + { + client = await _listener.AcceptTcpClientAsync(ct); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[TLS:{Port}] Accept error: {ex.Message}"); + continue; + } + + _ = HandleClientAsync(client, ct); + } + } + + private async Task HandleClientAsync(TcpClient client, CancellationToken ct) + { + using (client) + { + SslStream? sslStream = null; + try + { + sslStream = new SslStream(client.GetStream(), leaveInnerStreamOpen: false); + + await sslStream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions + { + ServerCertificate = _serverCert, + ClientCertificateRequired = false, + EnabledSslProtocols = SslProtocols.Tls12, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck + }, ct); + + Console.WriteLine($"[TLS:{Port}] Client connected, TLS {sslStream.SslProtocol}"); + + var buf = new byte[4096]; + int read; + + while ((read = await sslStream.ReadAsync(buf, ct)) > 0) + { + await sslStream.WriteAsync(buf.AsMemory(0, read), ct); + } + } + catch (OperationCanceledException) { } + catch (IOException) { } + catch (Exception ex) + { + Console.Error.WriteLine($"[TLS:{Port}] TLS error: {ex.Message}"); + } + finally + { + if (sslStream != null) + { + await sslStream.DisposeAsync(); + } + } + } + } + + public void Dispose() + { + _cts.Cancel(); + _listener.Stop(); + _acceptLoop?.Wait(TimeSpan.FromSeconds(2)); + _cts.Dispose(); + } +} diff --git a/Tests/SocketTests/CompanionClient.cs b/Tests/SocketTests/CompanionClient.cs index b93c03c..b8e7331 100644 --- a/Tests/SocketTests/CompanionClient.cs +++ b/Tests/SocketTests/CompanionClient.cs @@ -19,7 +19,7 @@ namespace NFUnitTestSocketTests internal sealed class CompanionClient : IDisposable { private readonly Socket _socket; - private readonly byte[] _recvBuf = new byte[128]; + private readonly byte[] _recvBuf = new byte[256]; internal CompanionClient() { @@ -67,6 +67,34 @@ internal bool ConnectTo(string host, int port) return response.IndexOf("\"ok\":true") >= 0; } + /// Asks the companion to start a TLS echo server on the given port. + internal bool StartTlsEcho(int port) + { + var response = SendCommand("{\"cmd\":\"start_tls_echo\",\"port\":" + port + "}"); + return response.IndexOf("\"ok\":true") >= 0; + } + + /// Asks the companion to open a TLS connection to the MCU acting as TLS server. + internal bool TlsConnectTo(string host, int port) + { + var response = SendCommand("{\"cmd\":\"tls_connect_to\",\"host\":\"" + host + "\",\"port\":" + port + "}"); + return response.IndexOf("\"ok\":true") >= 0; + } + + /// + /// Asks the companion to open a TLS connection to the MCU, send the given data, and + /// read back the echo. The TLS handshake and data exchange happen in the background + /// on the companion; this returns true as soon as the TCP connection is established + /// (the device drives the echo and verifies the payload on its side). + /// + internal bool TlsConnectEcho(string host, int port, byte[] data) + { + string dataB64 = Convert.ToBase64String(data); + var resp = SendCommand("{\"cmd\":\"tls_connect_echo\",\"host\":\"" + host + "\",\"port\":" + port + ",\"data\":\"" + dataB64 + "\"}"); + + return resp.IndexOf("\"ok\":true") >= 0; + } + private string SendCommand(string json) { byte[] cmd = Encoding.UTF8.GetBytes(json + "\n"); @@ -75,8 +103,27 @@ private string SendCommand(string json) // Give the companion time to act and respond Thread.Sleep(100); - int received = _socket.Receive(_recvBuf); - return new string(Encoding.UTF8.GetChars(_recvBuf, 0, received)); + // Read until we get a newline (end of JSON response) + string result = ""; + int maxAttempts = 50; + + while (maxAttempts-- > 0) + { + int received = _socket.Receive(_recvBuf); + if (received > 0) + { + result += new string(Encoding.UTF8.GetChars(_recvBuf, 0, received)); + + if (result.IndexOf("\n") >= 0) + { + break; + } + } + + Thread.Sleep(50); + } + + return result; } public void Dispose() => _socket.Close(); diff --git a/Tests/SocketTests/SocketTests.nfproj b/Tests/SocketTests/SocketTests.nfproj index fbc60d1..d080e78 100644 --- a/Tests/SocketTests/SocketTests.nfproj +++ b/Tests/SocketTests/SocketTests.nfproj @@ -62,7 +62,6 @@ - - + - + - <_CompanionIP>@(_CompanionIPItems) <_CompanionPort>@(_CompanionPortItems) <_CompanionIP Condition="'$(_CompanionIP)' == ''">127.0.0.1 <_CompanionPort Condition="'$(_CompanionPort)' == ''">11000 - <_TestConfigLines Include="// Auto-generated from .runsettings - do not edit directly." /> <_TestConfigLines Include="//" /> @@ -101,19 +96,13 @@ <_TestConfigLines Include="%20%20%20%20}" /> <_TestConfigLines Include="}" /> - - - + - - + \ No newline at end of file diff --git a/Tests/SslStreamTests/Properties/AssemblyInfo.cs b/Tests/SslStreamTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..39e4bc2 --- /dev/null +++ b/Tests/SslStreamTests/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright (c) 2025 nanoFramework contributors")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Tests/SslStreamTests/SslStreamTests.nfproj b/Tests/SslStreamTests/SslStreamTests.nfproj new file mode 100644 index 0000000..6ab274f --- /dev/null +++ b/Tests/SslStreamTests/SslStreamTests.nfproj @@ -0,0 +1,109 @@ + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + b2c3d4e5-f6a7-8901-bcde-f12345678901 + Library + Properties + 512 + NFUnitTestSslStream + NFUnitTest + False + true + UnitTest + v1.0 + true + true + + + + + SslClientTests.cs + + + SslServerTests.cs + + + CompanionClient.cs + + + + + + + ..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\..\packages\nanoFramework.TestFramework.3.0.80\lib\nanoFramework.TestFramework.dll + + + ..\..\packages\nanoFramework.TestFramework.3.0.80\lib\nanoFramework.UnitTestLauncher.exe + + + ..\..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + + + + + + + + + + + + + + + + <_CompanionIP>@(_CompanionIPItems) + <_CompanionPort>@(_CompanionPortItems) + <_CompanionIP Condition="'$(_CompanionIP)' == ''">127.0.0.1 + <_CompanionPort Condition="'$(_CompanionPort)' == ''">11000 + + + <_TestConfigLines Include="// Auto-generated from .runsettings - do not edit directly." /> + <_TestConfigLines Include="//" /> + <_TestConfigLines Include="// Copyright (c) .NET Foundation and Contributors" /> + <_TestConfigLines Include="// See LICENSE file in the project root for full license information." /> + <_TestConfigLines Include="//" /> + <_TestConfigLines Include="namespace NFUnitTestSocketTests" /> + <_TestConfigLines Include="{" /> + <_TestConfigLines Include="%20%20%20%20internal static class TestConfiguration" /> + <_TestConfigLines Include="%20%20%20%20{" /> + <_TestConfigLines Include="%20%20%20%20%20%20%20%20internal const string CompanionIP = "$(_CompanionIP)"%3B" /> + <_TestConfigLines Include="%20%20%20%20%20%20%20%20internal const int CompanionControlPort = $(_CompanionPort)%3B" /> + <_TestConfigLines Include="%20%20%20%20}" /> + <_TestConfigLines Include="}" /> + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/SslStreamTests/packages.config b/Tests/SslStreamTests/packages.config new file mode 100644 index 0000000..e5b4880 --- /dev/null +++ b/Tests/SslStreamTests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Tests/SslStreamTests/packages.lock.json b/Tests/SslStreamTests/packages.lock.json new file mode 100644 index 0000000..07b3059 --- /dev/null +++ b/Tests/SslStreamTests/packages.lock.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, + "nanoFramework.TestFramework": { + "type": "Direct", + "requested": "[3.0.80, 3.0.80]", + "resolved": "3.0.80", + "contentHash": "o2ymxAz6TC6VguKqN6rrGB/L4sVAIJVEut87Hq8MvwEe6P/7JwzX+VB4vKJxEmihCSJs7Z3Yo9Rb8HgIEo8DMg==" + } + } + } +} \ No newline at end of file diff --git a/Tests/SslStreamTests_WiFi/Properties/AssemblyInfo.cs b/Tests/SslStreamTests_WiFi/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..39e4bc2 --- /dev/null +++ b/Tests/SslStreamTests_WiFi/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright (c) 2025 nanoFramework contributors")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Tests/SslStreamTests_WiFi/SslClientTests.cs b/Tests/SslStreamTests_WiFi/SslClientTests.cs new file mode 100644 index 0000000..eb1493a --- /dev/null +++ b/Tests/SslStreamTests_WiFi/SslClientTests.cs @@ -0,0 +1,401 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.Networking; +using nanoFramework.TestFramework; +using System; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; + +#if HAS_WIFI +using System.Device.Wifi; +#endif + +namespace NFUnitTestSslStream +{ + /// + /// Exercises the device acting as a TLS client. + /// + [TestClass] + public class SslClientTests + { + private const string TestHost = "www.howsmyssl.com"; + private const int HttpsPort = 443; + + private static bool _networkInitialized = false; + + [Setup] + public void Setup() + { + // Comment the next line to run these tests on real hardware with network connectivity + Assert.SkipTest("Skipping SSL tests: requires real hardware with network connectivity"); + + CancellationTokenSource cs = new(60000); + +#if HAS_WIFI + bool connected = WifiNetworkHelper.Reconnect(requiresDateTime: true, token: cs.Token); +#else + bool connected = NetworkHelper.SetupAndConnectNetwork(requiresDateTime: true, token: cs.Token); +#endif + + if (!connected) + { + Assert.SkipTest($"Network not available ({NetworkHelper.Status}) - skipping SSL tests"); + } + + _networkInitialized = true; + OutputHelper.WriteLine($"Network ready, status: {NetworkHelper.Status}"); + } + + [Cleanup] + public void Cleanup() + { + if (_networkInitialized) + { + NetworkHelper.Reset(); + _networkInitialized = false; + } + } + + [TestMethod] + public void ConnectToSecureServer_WithNoVerification_Succeeds_Tls12() + { + ConnectToSecureServer_WithNoVerification_Succeeds(SslProtocols.Tls12); + } + + [TestMethod] + public void ConnectToSecureServer_WithNoVerification_Succeeds_Tls13() + { + ConnectToSecureServer_WithNoVerification_Succeeds(SslProtocols.Tls13); + } + + private void ConnectToSecureServer_WithNoVerification_Succeeds(SslProtocols protocol) + { + using (Socket socket = ConnectSocket(TestHost, HttpsPort)) + using (SslStream sslStream = new SslStream(socket)) + { + try + { + // Skip certificate verification - focuses on handshake mechanics + sslStream.SslVerification = SslVerification.NoVerification; + sslStream.AuthenticateAsClient(TestHost, protocol); + + OutputHelper.WriteLine($"TLS handshake succeeded (no verification, {protocol})"); + + string response = SendHttpGetAndReadResponse(sslStream); + + Assert.IsTrue( + response.IndexOf("tls_version") > -1, + "Response should contain tls_version confirming TLS negotiation"); + + OutputHelper.WriteLine("Successfully received TLS check response"); + } + catch (InvalidOperationException ex) + { + // Handshake context errors: HandshakeBadContext, HandshakeSetHostname + OutputHelper.WriteLine($"InvalidOperationException: {ex.Message}"); + throw; + } + catch (CryptographicException ex) + { + // HandshakeCertVerifyFailed: ErrorCode = MBEDTLS_X509_BADCERT_* bitmask + // HandshakeFailed: ErrorCode = raw negative mbedTLS error code + OutputHelper.WriteLine($"CryptographicException: {ex.Message}"); + OutputHelper.WriteLine($" ErrorCode: 0x{ex.ErrorCode:X8}"); + throw; + } + } + } + + [TestMethod] + public void ConnectToSecureServer_WithCertVerification_Succeeds_Tls12() + { + ConnectToSecureServer_WithCertVerification_Succeeds(SslProtocols.Tls12); + } + + [TestMethod] + public void ConnectToSecureServer_WithCertVerification_Succeeds_Tls13() + { + ConnectToSecureServer_WithCertVerification_Succeeds(SslProtocols.Tls13); + } + + private void ConnectToSecureServer_WithCertVerification_Succeeds(SslProtocols protocol) + { + X509Certificate caCert = new X509Certificate(ISRGRootX1); + + using (Socket socket = ConnectSocket(TestHost, HttpsPort)) + using (SslStream sslStream = new SslStream(socket)) + { + try + { + // Provide the CA root certificate for the server + sslStream.AuthenticateAsClient(TestHost, null, caCert, protocol); + + OutputHelper.WriteLine($"TLS handshake succeeded (with certificate verification, {protocol})"); + + string response = SendHttpGetAndReadResponse(sslStream); + + Assert.IsTrue( + response.IndexOf("tls_version") > -1, + "Response should contain tls_version confirming TLS negotiation"); + + OutputHelper.WriteLine("Successfully received TLS check response with cert verification"); + } + catch (InvalidOperationException ex) + { + OutputHelper.WriteLine($"InvalidOperationException: {ex.Message}"); + throw; + } + catch (CryptographicException ex) + { + OutputHelper.WriteLine($"CryptographicException: {ex.Message}"); + OutputHelper.WriteLine($" ErrorCode: 0x{ex.ErrorCode:X8}"); + throw; + } + } + } + + [TestMethod] + public void ConnectWithWrongCaCert_ThrowsCryptographicException_Tls12() + { + ConnectWithWrongCaCert_ThrowsCryptographicException(SslProtocols.Tls12); + } + + [TestMethod] + public void ConnectWithWrongCaCert_ThrowsCryptographicException_Tls13() + { + ConnectWithWrongCaCert_ThrowsCryptographicException(SslProtocols.Tls13); + } + + private void ConnectWithWrongCaCert_ThrowsCryptographicException(SslProtocols protocol) + { + // Use a self-signed cert that is NOT the CA for howsmyssl.com + X509Certificate wrongCaCert = new X509Certificate(InvalidCACert); + + using (Socket socket = ConnectSocket(TestHost, HttpsPort)) + using (SslStream sslStream = new SslStream(socket)) + { + bool exceptionCaught = false; + + try + { + // Provide a wrong CA certificate - the server cert chain won't validate + sslStream.AuthenticateAsClient(TestHost, null, wrongCaCert, protocol); + } + catch (CryptographicException ex) + { + exceptionCaught = true; + + OutputHelper.WriteLine($"Expected CryptographicException caught"); + OutputHelper.WriteLine($" Message: {ex.Message}"); + OutputHelper.WriteLine($" ErrorCode: 0x{ex.ErrorCode:X8}"); + + // ErrorCode should contain MBEDTLS_X509_BADCERT_NOT_TRUSTED (0x08) or similar + Assert.IsTrue( + ex.ErrorCode != 0, + "ErrorCode should be non-zero (MBEDTLS_X509_BADCERT_* bitmask)"); + } + + Assert.IsTrue(exceptionCaught, "Expected an exception when using wrong CA certificate"); + } + } + + [TestMethod] + public void ConnectWithBadHostname_ThrowsCryptographicException_Tls12() + { + ConnectWithBadHostname_ThrowsCryptographicException(SslProtocols.Tls12); + } + + [TestMethod] + public void ConnectWithBadHostname_ThrowsCryptographicException_Tls13() + { + ConnectWithBadHostname_ThrowsCryptographicException(SslProtocols.Tls13); + } + + private void ConnectWithBadHostname_ThrowsCryptographicException(SslProtocols protocol) + { + X509Certificate caCert = new X509Certificate(ISRGRootX1); + + using (Socket socket = ConnectSocket(TestHost, HttpsPort)) + using (SslStream sslStream = new SslStream(socket)) + { + bool exceptionCaught = false; + + try + { + // Authenticate with a mismatched hostname - server cert won't match, + // triggering HandshakeCertVerifyFailed with MBEDTLS_X509_BADCERT_CN_MISMATCH + sslStream.AuthenticateAsClient("wrong.host.name", null, caCert, protocol); + } + catch (CryptographicException ex) + { + exceptionCaught = true; + + OutputHelper.WriteLine($"Expected CryptographicException caught"); + OutputHelper.WriteLine($" Message: {ex.Message}"); + OutputHelper.WriteLine($" ErrorCode: 0x{ex.ErrorCode:X8}"); + + Assert.IsTrue( + ex.ErrorCode != 0, + "ErrorCode should be non-zero (MBEDTLS_X509_BADCERT_* bitmask)"); + } + + Assert.IsTrue(exceptionCaught, "Expected an exception from hostname mismatch authentication"); + } + } + + [TestMethod] + public void ConnectWithoutAuth_ThrowsCryptographicException_Tls12() + { + ConnectWithoutAuth_ThrowsCryptographicException(SslProtocols.Tls12); + } + + [TestMethod] + public void ConnectWithoutAuth_ThrowsCryptographicException_Tls13() + { + ConnectWithoutAuth_ThrowsCryptographicException(SslProtocols.Tls13); + } + + private void ConnectWithoutAuth_ThrowsCryptographicException(SslProtocols protocol) + { + using (Socket socket = ConnectSocket(TestHost, HttpsPort)) + using (SslStream sslStream = new SslStream(socket)) + { + bool exceptionCaught = false; + + try + { + // Certificate verification required (default) but no CA certificate provided + // and no device certificate store - should fail with HandshakeCertVerifyFailed + sslStream.AuthenticateAsClient(TestHost, protocol); + } + catch (CryptographicException ex) + { + exceptionCaught = true; + + OutputHelper.WriteLine($"Expected CryptographicException caught"); + OutputHelper.WriteLine($" Message: {ex.Message}"); + OutputHelper.WriteLine($" ErrorCode: 0x{ex.ErrorCode:X8}"); + + Assert.IsTrue( + ex.ErrorCode != 0, + "ErrorCode should be non-zero (certificate verification failure flags)"); + } + + Assert.IsTrue(exceptionCaught, "Expected an exception when connecting without authentication"); + } + } + + /// + /// Creates a TCP socket and connects to the specified host and port. + /// + private static Socket ConnectSocket(string host, int port) + { + IPHostEntry hostEntry = Dns.GetHostEntry(host); + IPEndPoint ep = new IPEndPoint(hostEntry.AddressList[0], port); + + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(ep); + + return socket; + } + + /// + /// Sends an HTTP GET request and reads the response. + /// + private static string SendHttpGetAndReadResponse(SslStream sslStream) + { + byte[] request = Encoding.UTF8.GetBytes( + $"GET /a/check HTTP/1.1\r\nHost: {TestHost}\r\nConnection: close\r\n\r\n"); + + sslStream.Write(request, 0, request.Length); + + // Use blocking Read - the native ReadWriteHelper handles non-blocking + // sockets with WaitEvents, so Read() will block until data arrives + // or the ReceiveTimeout expires. No need to poll DataAvailable. + byte[] buffer = new byte[1024]; + StringBuilder sb = new StringBuilder(); + int bytesRead; + + do + { + bytesRead = sslStream.Read(buffer, 0, buffer.Length); + + if (bytesRead > 0) + { + sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + } + } + while (bytesRead > 0); + + string response = sb.ToString(); + OutputHelper.WriteLine($"Response length: {response.Length} bytes"); + + return response; + } + + // ISRG Root X1 - root CA for Let's Encrypt (used by www.howsmyssl.com) + // from https://letsencrypt.org/certificates/ + private const string ISRGRootX1 = +@"-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE-----"; + + // Frank4DD Web CA — a valid certificate that is NOT the CA for howsmyssl.com. + // Using it as the CA cert should trigger MBEDTLS_X509_BADCERT_NOT_TRUSTED. + private const string InvalidCACert = +@"-----BEGIN CERTIFICATE----- +MIIC2jCCAkMCAg38MA0GCSqGSIb3DQEBBQUAMIGbMQswCQYDVQQGEwJKUDEOMAwG +A1UECBMFVG9reW8xEDAOBgNVBAcTB0NodW8ta3UxETAPBgNVBAoTCEZyYW5rNERE +MRgwFgYDVQQLEw9XZWJDZXJ0IFN1cHBvcnQxGDAWBgNVBAMTD0ZyYW5rNEREIFdl +YiBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmcmFuazRkZC5jb20wHhcNMTIw +ODIyMDUyNzQxWhcNMTcwODIxMDUyNzQxWjBKMQswCQYDVQQGEwJKUDEOMAwGA1UE +CAwFVG9reW8xETAPBgNVBAoMCEZyYW5rNEREMRgwFgYDVQQDDA93d3cuZXhhbXBs +ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0z9FeMynsC8+u +dvX+LciZxnh5uRj4C9S6tNeeAlIGCfQYk0zUcNFCoCkTknNQd/YEiawDLNbxBqut +bMDZ1aarys1a0lYmUeVLCIqvzBkPJTSQsCopQQ9V8WuT252zzNzs68dVGNdCJd5J +NRQykpwexmnjPPv0mvj7i8XgG379TyW6P+WWV5okeUkXJ9eJS2ouDYdR2SM9BoVW ++FgxDu6BmXhozW5EfsnajFp7HL8kQClI0QOc79yuKl3492rH6bzFsFn2lfwWy9ic +7cP8EpCTeFp1tFaD+vxBhPZkeTQ1HKx6hQ5zeHIB5ySJJZ7af2W8r4eTGYzbdRW2 +4DDHCPhZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAQMv+BFvGdMVzkQaQ3/+2noVz +/uAKbzpEL8xTcxYyP3lkOeh4FoxiSWqy5pGFALdPONoDuYFpLhjJSZaEwuvjI/Tr +rGhLV1pRG9frwDFshqD2Vaj4ENBCBh6UpeBop5+285zQ4SI7q4U9oSebUDJiuOx6 ++tZ9KynmrbJpTSi0+BM= +-----END CERTIFICATE-----"; + } +} diff --git a/Tests/SslStreamTests_WiFi/SslServerTests.cs b/Tests/SslStreamTests_WiFi/SslServerTests.cs new file mode 100644 index 0000000..bc3388e --- /dev/null +++ b/Tests/SslStreamTests_WiFi/SslServerTests.cs @@ -0,0 +1,602 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.Networking; +using nanoFramework.TestFramework; +using NFUnitTestSocketTests; +using System; +using System.IO; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; + +#if HAS_WIFI +using System.Device.Wifi; +#endif + +namespace NFUnitTestSslStream +{ + /// + /// Exercises the device acting as a TLS server (AuthenticateAsServer / SecureAccept) + /// and as a TLS client against the Network Test Companion. + /// + /// The device loads a static self-signed test certificate (embedded below) to serve. + /// The companion connects as a TLS client with certificate verification disabled. + /// For the reverse direction the companion runs a TLS echo server (using the same + /// static certificate) and the device connects with . + /// + /// Requires the Network Test Companion running on the host PC. Start it with: + /// dotnet run --project Tests/NetworkTestCompanion + /// + [TestClass] + public class SslServerTests + { + private static bool _networkInitialized = false; + + [Setup] + public void Setup() + { + // Comment the next line to run these tests on real hardware with network connectivity + Assert.SkipTest("Skipping SSL server tests: requires real hardware with companion running"); + + CancellationTokenSource cs = new(60000); + +#if HAS_WIFI + bool connected = WifiNetworkHelper.Reconnect(requiresDateTime: true, token: cs.Token); +#else + bool connected = NetworkHelper.SetupAndConnectNetwork(requiresDateTime: true, token: cs.Token); +#endif + + if (!connected) + { + Assert.SkipTest($"Network not available ({NetworkHelper.Status}) - skipping SSL server tests"); + } + + _networkInitialized = true; + OutputHelper.WriteLine($"Network ready, status: {NetworkHelper.Status}"); + } + + [Cleanup] + public void Cleanup() + { + if (_networkInitialized) + { + NetworkHelper.Reset(); + _networkInitialized = false; + } + } + + [TestMethod] + public void DeviceAsServer_CompanionConnects_HandshakeSucceeds() + { + const int port = 7010; + + X509Certificate2 serverCert = new X509Certificate2(ServerCertPem, ServerKeyPem, null); + + using (CompanionClient companion = new CompanionClient()) + { + Assert.IsTrue(companion.Ping(), "Companion not reachable"); + + Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, port)); + listener.Listen(1); + + try + { + string mcuIp = GetLocalIp(); + + Assert.IsTrue(companion.TlsConnectTo(mcuIp, port), "TlsConnectTo command failed"); + + Assert.IsTrue(listener.Poll(10 * 1000 * 1000, SelectMode.SelectRead), + "No connection received from companion within timeout"); + + Socket accepted = listener.Accept(); + + try + { + using (SslStream sslStream = new SslStream(accepted)) + { + AuthenticateAsServerLogged(sslStream, serverCert); + OutputHelper.WriteLine("TLS server handshake succeeded"); + } + } + finally + { + accepted.Close(); + } + } + finally + { + listener.Close(); + } + } + } + + [TestMethod] + public void DeviceAsServer_TlsEchoRoundTrip() + { + const int port = 7011; + byte[] testData = Encoding.UTF8.GetBytes("Hello from companion to device TLS server!"); + + X509Certificate2 serverCert = new X509Certificate2(ServerCertPem, ServerKeyPem, null); + + using (CompanionClient companion = new CompanionClient()) + { + Assert.IsTrue(companion.Ping(), "Companion not reachable"); + + Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, port)); + listener.Listen(1); + + try + { + string mcuIp = GetLocalIp(); + + // Companion connects, handshakes, sends data and reads the echo (all in + // the background); returns Ok as soon as the TCP connection is up. + Assert.IsTrue(companion.TlsConnectEcho(mcuIp, port, testData), + "TlsConnectEcho command failed"); + + Assert.IsTrue(listener.Poll(10 * 1000 * 1000, SelectMode.SelectRead), + "No connection received from companion within timeout"); + + Socket accepted = listener.Accept(); + + try + { + using (SslStream sslStream = new SslStream(accepted)) + { + AuthenticateAsServerLogged(sslStream, serverCert); + OutputHelper.WriteLine("TLS server handshake succeeded"); + + byte[] buffer = new byte[testData.Length]; + int bytesRead = 0; + + while (bytesRead < testData.Length) + { + int n = sslStream.Read(buffer, bytesRead, testData.Length - bytesRead); + if (n == 0) + { + break; + } + + bytesRead += n; + } + + Assert.AreEqual(testData.Length, bytesRead, "Should receive exact bytes sent by companion"); + + for (int i = 0; i < bytesRead; i++) + { + Assert.AreEqual(testData[i], buffer[i], $"Data mismatch at byte {i}"); + } + + // Echo it back to the companion + sslStream.Write(buffer, 0, bytesRead); + + OutputHelper.WriteLine($"Echoed {bytesRead} bytes back to companion"); + } + } + finally + { + accepted.Close(); + } + } + finally + { + listener.Close(); + } + } + } + + [TestMethod] + public void DeviceAsClient_TlsEchoRoundTrip() + { + const int port = 7012; + + using (CompanionClient companion = new CompanionClient()) + { + Assert.IsTrue(companion.Ping(), "Companion not reachable"); + Assert.IsTrue(companion.StartTlsEcho(port), "StartTlsEcho command failed"); + + try + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(new IPEndPoint( + IPAddress.Parse(TestConfiguration.CompanionIP), + port)); + + using (SslStream sslStream = new SslStream(socket)) + { + sslStream.SslVerification = SslVerification.NoVerification; + sslStream.AuthenticateAsClient("nanoFramework Test Server", SslProtocols.Tls12); + + OutputHelper.WriteLine("TLS client handshake to companion succeeded"); + + byte[] sent = Encoding.UTF8.GetBytes("Hello TLS echo from device!"); + sslStream.Write(sent, 0, sent.Length); + + byte[] buffer = new byte[sent.Length]; + int bytesRead = 0; + + while (bytesRead < sent.Length) + { + int n = sslStream.Read(buffer, bytesRead, sent.Length - bytesRead); + if (n == 0) + { + break; + } + + bytesRead += n; + } + + Assert.AreEqual(sent.Length, bytesRead, "Echo response length should match sent data"); + + for (int i = 0; i < sent.Length; i++) + { + Assert.AreEqual(sent[i], buffer[i], $"Echo mismatch at byte {i}"); + } + + OutputHelper.WriteLine($"TLS echo round-trip succeeded: {bytesRead} bytes"); + } + } + finally + { + companion.Stop(port); + } + } + } + + [TestMethod] + public void DeviceAsClient_MultiChunk_TlsEchoRoundTrip() + { + const int port = 7013; + // 8 KB total, exchanged as a chunked ping-pong: write a chunk, read its echo + // back, then send the next chunk. This spans many SecureWrite/SecureRead calls + // (and multiple TLS records) while never keeping more than one chunk in flight. + // A single Write(8192) followed by a read-all would risk a half-duplex echo + // stall on constrained devices, since neither side drains the other's echo + // until the whole payload is buffered. + const int totalSize = 8 * 1024; + const int chunkSize = 1024; + + using (CompanionClient companion = new CompanionClient()) + { + Assert.IsTrue(companion.Ping(), "Companion not reachable"); + Assert.IsTrue(companion.StartTlsEcho(port), "StartTlsEcho command failed"); + + try + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(new IPEndPoint( + IPAddress.Parse(TestConfiguration.CompanionIP), + port)); + + using (SslStream sslStream = new SslStream(socket)) + { + sslStream.SslVerification = SslVerification.NoVerification; + sslStream.AuthenticateAsClient("nanoFramework Test Server", SslProtocols.Tls12); + + byte[] chunk = new byte[chunkSize]; + byte[] received = new byte[chunkSize]; + int grandTotal = 0; + + for (int sentSoFar = 0; sentSoFar < totalSize; sentSoFar += chunkSize) + { + // Fill the chunk with a position-dependent pattern so a + // mis-ordered or corrupted echo is detected. + for (int i = 0; i < chunkSize; i++) + { + chunk[i] = (byte)((sentSoFar + i) & 0xFF); + } + + sslStream.Write(chunk, 0, chunkSize); + + // Read this chunk's echo fully before sending the next one. + int chunkRead = 0; + while (chunkRead < chunkSize) + { + int bytesRead = sslStream.Read(received, chunkRead, chunkSize - chunkRead); + if (bytesRead == 0) + { + break; + } + + chunkRead += bytesRead; + } + + Assert.AreEqual(chunkSize, chunkRead, $"Short echo for chunk at offset {sentSoFar}"); + + for (int i = 0; i < chunkSize; i++) + { + Assert.AreEqual(chunk[i], received[i], + $"Echo mismatch at byte {sentSoFar + i}"); + } + + grandTotal += chunkRead; + + OutputHelper.WriteLine($"Chunk {sentSoFar / chunkSize + 1}/{totalSize / chunkSize}: echoed {chunkRead} bytes OK (total {grandTotal}/{totalSize})"); + } + + Assert.AreEqual(totalSize, grandTotal, "Should echo the whole payload back"); + + OutputHelper.WriteLine($"Multi-chunk TLS echo round-trip succeeded: {grandTotal} bytes"); + } + } + finally + { + companion.Stop(port); + } + } + } + + [TestMethod] + public void DeviceAsClient_WriteAfterPeerClose_ShouldThrow() + { + // Regression test: SslStream.Write used to discard the error returned by + // native ssl_write_internal, so writing to a dead connection returned + // normally and the data was silently lost. A write to a peer that has + // gone away must now fail rather than silently succeed. + const int port = 7015; + + using (CompanionClient companion = new CompanionClient()) + { + Assert.IsTrue(companion.Ping(), "Companion not reachable"); + Assert.IsTrue(companion.StartTlsEcho(port), "StartTlsEcho command failed"); + + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(new IPEndPoint( + IPAddress.Parse(TestConfiguration.CompanionIP), + port)); + + using (SslStream sslStream = new SslStream(socket)) + { + sslStream.SslVerification = SslVerification.NoVerification; + sslStream.AuthenticateAsClient("nanoFramework Test Server", SslProtocols.Tls12); + + // Bound how long a write can block. On a dead peer the native write path waits + // for the socket to become writable; without a finite timeout it blocks + // indefinitely (hanging the test) instead of failing. Must be set now, while the + // socket is still alive - setsockopt on an already-reset socket throws. + socket.SendTimeout = 3000; + + // 1. Prove the connection works with one echo round-trip + byte[] probe = Encoding.UTF8.GetBytes("alive"); + sslStream.Write(probe, 0, probe.Length); + + byte[] probeBack = new byte[probe.Length]; + int probeRead = 0; + while (probeRead < probe.Length) + { + int n = sslStream.Read( + probeBack, + probeRead, + probe.Length - probeRead); + + if (n == 0) + { + break; + } + + probeRead += n; + } + + Assert.AreEqual(probe.Length, probeRead, "Initial echo round-trip failed"); + OutputHelper.WriteLine("Initial echo round-trip OK - connection is live"); + + // 2. Kill the companion echo server, tearing down the peer TLS session + Assert.IsTrue(companion.Stop(port), "Stop command failed"); + OutputHelper.WriteLine("Companion echo server stopped - peer connection is dead"); + + // Let the TCP RST / FIN propagate + Thread.Sleep(2000); + + // 3. Write to the dead stream - this SHOULD throw once the connection is known bad. + // A single write is not a reliable trigger: on a freshly-closed TCP peer the first + // send() after close usually succeeds locally (bytes are just queued in the local + // send buffer) and the failure only surfaces on a later write, once the RST has + // been received or the send buffer can't drain. So write repeatedly - each call + // bounded by SendTimeout - until one fails. Only the two documented failure modes + // (native SocketException, or the managed IOException guard) are normalized to + // IOException; any other exception type is left to propagate so an unrelated bug + // fails the test loudly instead of being reported as the expected outcome. + byte[] payload = Encoding.UTF8.GetBytes("this should fail"); + const int maxAttempts = 5; + + Assert.ThrowsException( + typeof(IOException), + () => + { + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + sslStream.Write(payload, 0, payload.Length); + OutputHelper.WriteLine($"Attempt {attempt}/{maxAttempts}: Write({payload.Length} bytes) returned without error on a dead connection"); + Thread.Sleep(500); + } + catch (IOException) + { + OutputHelper.WriteLine($"Attempt {attempt}/{maxAttempts}: Write failed as expected: IOException"); + throw; + } + catch (SocketException ex) + { + OutputHelper.WriteLine($"Attempt {attempt}/{maxAttempts}: Write failed as expected: {ex.GetType().Name}: {ex.Message}"); + throw new IOException("Write failed on dead connection", ex); + } + } + }, + $"SslStream.Write should have thrown on a dead connection within {maxAttempts} attempts but returned silently every time - data was lost"); + } + + // Explicit cleanup + socket.Close(); + Thread.Sleep(500); + } + } + + [TestMethod] + public void DeviceAsServer_MultipleSequentialConnections() + { + const int port = 7014; + const int connectionCount = 3; + + using (CompanionClient companion = new CompanionClient()) + { + Assert.IsTrue(companion.Ping(), "Companion not reachable"); + + Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, port)); + listener.Listen(1); + + try + { + string mcuIp = GetLocalIp(); + + // Load the certificate once and reuse it across connections. Allocating it + // per iteration (each decoding the private key natively) wastes memory and + // risks heap fragmentation on the device. + X509Certificate2 serverCert = new X509Certificate2(ServerCertPem, ServerKeyPem, null); + + for (int i = 0; i < connectionCount; i++) + { + OutputHelper.WriteLine($"Connection {i + 1}/{connectionCount}"); + + Assert.IsTrue(companion.TlsConnectTo(mcuIp, port), + $"TlsConnectTo command failed on iteration {i + 1}"); + + Assert.IsTrue(listener.Poll(10 * 1000 * 1000, SelectMode.SelectRead), + $"No connection on iteration {i + 1}"); + + Socket accepted = listener.Accept(); + + try + { + using (SslStream sslStream = new SslStream(accepted)) + { + AuthenticateAsServerLogged(sslStream, serverCert); + OutputHelper.WriteLine($"Handshake {i + 1} succeeded"); + } + } + finally + { + accepted.Close(); + } + + // Brief pause between connections to let the SSL context slot be freed + Thread.Sleep(500); + } + + OutputHelper.WriteLine($"All {connectionCount} sequential TLS connections succeeded"); + } + finally + { + listener.Close(); + } + } + } + + // Wraps AuthenticateAsServer to surface the native error on failure. + // Passes clientCertificateRequired: false so the server does NOT ask the client + // for a certificate - otherwise the default SslVerification.CertificateRequired + // makes mbedTLS abort the handshake with -0x7480 (NO_CLIENT_CERTIFICATE). + // On failure, ErrorCode is either a small SslError enum value (context init: + // 6=key, 7=cert, 8=own-cert-config, 9=setup/OOM) or, during the handshake, the + // raw negative mbedTLS error code (e.g. -0x7480). + private static void AuthenticateAsServerLogged(SslStream sslStream, X509Certificate2 serverCert) + { + try + { + sslStream.AuthenticateAsServer(serverCert, false, SslProtocols.Tls12); + } + catch (CryptographicException ex) + { + OutputHelper.WriteLine($"AuthenticateAsServer failed: ErrorCode={ex.ErrorCode}"); + throw; + } + } + + private static string GetLocalIp() + { + foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) + { + if (ni.IPv4Address == null || ni.IPv4Address == "0.0.0.0") + { + continue; + } + + if (ni.IPv4Address.StartsWith("127.") || ni.IPv4Address.StartsWith("169.254.")) + { + continue; + } + + return ni.IPv4Address; + } + + return "0.0.0.0"; + } + + // Static self-signed RSA-2048 test certificate, CN=nanoFramework Test Server. + // The SAME cert/key is embedded in the companion (TestCertificates.cs). + // Throw-away test certificate - not a secret. Valid 2026..2036. + private const string ServerCertPem = +@"-----BEGIN CERTIFICATE----- +MIIDRTCCAi2gAwIBAgIUepRBLWtpFLvLv6rjIIUQxmkVkYkwDQYJKoZIhvcNAQEL +BQAwJDEiMCAGA1UEAwwZbmFub0ZyYW1ld29yayBUZXN0IFNlcnZlcjAeFw0yNjA3 +MTYxMjA0MDJaFw0zNjA3MTMxMjA0MDJaMCQxIjAgBgNVBAMMGW5hbm9GcmFtZXdv +cmsgVGVzdCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu ++yM+X9ZcaawdwfhpJiWa4qlrA/1aV0CoENchMP6XOr4Eq7h/Y8jH+QlKdG2hFe31 +wULiwLJq6QwTQ23a7vRFBgTZCZJSs5QY54o2r7O6pO37Y1w+/d0/4blFLNWd0PQq +Mm8TUKdK3J11dv+n/oY9++4vFHR6Bo3xjHFBvm03vcKETeF3UIX+g6J84lfNmdPs +A3UIFqkWXioC7a2+afnRczAHrrS0Py2KcSv+G5E94ZYQHs0VljY8CpOEV2maxh9S +Bjocv4o6HUejKoWvbXqkftuxztjYx77p++jhICpnNZjpNOb27rJhtGw3HPwtn8IY +I3jIZS72insBEQgKSxBhAgMBAAGjbzBtMB0GA1UdDgQWBBR7BiZTDR7gx4gl2fV1 +Y9fjrb1mMjAfBgNVHSMEGDAWgBR7BiZTDR7gx4gl2fV1Y9fjrb1mMjAJBgNVHRME +AjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEAIeCjTx10sJiacjzajLUJ/dfgSq8VfMUhHQLxG5VF3bPMVbr8LaF+ +sM77OKXOBKigDj65urMUwAoLKXF7UcewvVV239Og1p97acetDDiM3Q72duH8MFAF +K2V9qR6Cj8/jvQdSHWiJVOHE0u31PG6Xwa6aIwXA79VKbJaucvj9hXYz0O3HXTRc +RmvHemTge0p0VhuTL+wNv46mftEZhoSsPZa4S08nv5VX3EyWfQM6eX3ghnq6CsE+ +MQw94r5CV1kZDA/R9IdXh4aRIVCyN0ZmMvfNNrmIJRdr/eLzQc/6DBeh5Wrg7Sc/ +9oUAfyIiPqXG3sW0Txof2L5qbTmallrooQ== +-----END CERTIFICATE-----"; + + // PKCS#1 (traditional RSA) encoding - mbedTLS on the device configures the + // own-cert key from this format; PKCS#8 ("BEGIN PRIVATE KEY") fails SecureServerInit. + private const string ServerKeyPem = +@"-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArvsjPl/WXGmsHcH4aSYlmuKpawP9WldAqBDXITD+lzq+BKu4 +f2PIx/kJSnRtoRXt9cFC4sCyaukME0Nt2u70RQYE2QmSUrOUGOeKNq+zuqTt+2Nc +Pv3dP+G5RSzVndD0KjJvE1CnStyddXb/p/6GPfvuLxR0egaN8YxxQb5tN73ChE3h +d1CF/oOifOJXzZnT7AN1CBapFl4qAu2tvmn50XMwB660tD8tinEr/huRPeGWEB7N +FZY2PAqThFdpmsYfUgY6HL+KOh1HoyqFr216pH7bsc7Y2Me+6fvo4SAqZzWY6TTm +9u6yYbRsNxz8LZ/CGCN4yGUu9op7AREICksQYQIDAQABAoIBAB8L2Bj9EB+dcDhn +bhfZ+NoeVUjzkEQzLvmi40i0VLeoaIaToUyY+8rfWNKpDbqDFZGBFMj+v6lQaCAS +2q75rsWAZ+PKWvfpfOFeU5uYWR9InCD6ZCeZC2SGPEUVy2EQ7gF+qU6YBNa3hgiN +cJbyBgeBZ6Vaz7/G4fB1prKvgtlcunjtXAwdme9nkHR2kuG+pGGtNs/qc71bQeOt +5gphHdls+lHX+D6QD/gB2biR1bSJW+Cegz0zNM0nUUz6cA3K9dUTMeyGyI4c8FNt +u/FuRZlri/I7yUAPngVnmq46rJg1Ih0OvXI+jIEHp7SI0e9MiRLnZhB4x3/76940 +qAJ+nnECgYEA5692QqZ0eovLlqlWSrRNP0mG31HxCTrWYvfvfWkfhGPskdsJoOeQ +RXk5Mvfp84miIezv8aXxmQEquxQiP8HRtzQ0TrsvKdlM9XOGwUJULyqtFfSs6vmj +HTD16GGPxWqEy0xAcaQcM31pb8YwuZ5MCNRdhuL4ladWBfMR0z75GssCgYEAwVg8 +hPsX+H4LzCfu32MQCMh+sAR+1xPTLSG3ydJFbx6PE9pX408ZJKc9CtkG8Xz29+y1 +EZnymB1IUpNZxms/4pybyFaKXUa20SpPgSoIrBaL/wdgM4h4Pvs2FSceYN9qY7Z5 +d4DhJEiuez+CAFVqQnrNaLJI7xD094SEv91nQAMCgYA+rq8dOzG6UgYj3e61yXA4 +1ijCVMYUzDFil1fZI07en7ZKg+tn+B6FXVXHX2GRfUQ7T4Jfa5kg3zrzYHAftc2K +dnpMbsJE3UDAC6CCuvJRzIcFsKvz6tRhunRdib+/FqGU6y1oUZE7sQuMrR9TqOtD +XEltjAzbWGmitG+3Kot03wKBgQCunriaCgWOQpj5HB/b1aaHqDzzUDwWmCskGc3a +E3TudRUYAx1ZiPjWZ8zz3SsuM4UCSeEHMpkt1VSab8anM/oQ+wyflbmFoPZAVwxT +RdlrQznRbaHvKRQhHdWsqRYAvAdkY0u1KMsucA5V9fe9wWck/7BBHLROZmw4mJEk +kBxObQKBgQCCrcUNFhm3dxCdi+VgMwrhMOqiO5XYAGw4raQ/BbXwpb1PLbj4xtSZ +WVb3utTBP0WPhf58EcHc8ko4B+1xCMR4B9rntQACfngbUN4wETQ1Gz+bNgaHQyJo +gHYA38gnEOJurr2VLZFaqLgwj+7kpTRL2a0ZTDz8pCxlwDpsi4n1YA== +-----END RSA PRIVATE KEY-----"; + } +} diff --git a/Tests/SslStreamTests_WiFi/SslStreamTests_WiFi.nfproj b/Tests/SslStreamTests_WiFi/SslStreamTests_WiFi.nfproj new file mode 100644 index 0000000..d0525b4 --- /dev/null +++ b/Tests/SslStreamTests_WiFi/SslStreamTests_WiFi.nfproj @@ -0,0 +1,112 @@ + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + a1b2c3d4-e5f6-7890-abcd-ef1234567890 + Library + Properties + 512 + NFUnitTestSslStream + NFUnitTest + False + true + UnitTest + v1.0 + true + true + $(DefineConstants);HAS_WIFI; + + + + + + + CompanionClient.cs + + + + + + + ..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\..\packages\nanoFramework.TestFramework.3.0.80\lib\nanoFramework.TestFramework.dll + + + ..\..\packages\nanoFramework.TestFramework.3.0.80\lib\nanoFramework.UnitTestLauncher.exe + + + ..\..\packages\nanoFramework.Runtime.Events.1.11.37\lib\nanoFramework.Runtime.Events.dll + + + ..\..\packages\nanoFramework.System.Device.Wifi.1.5.149\lib\System.Device.Wifi.dll + + + ..\..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + + + + + + + + + + + + + + + + <_CompanionIP>@(_CompanionIPItems) + <_CompanionPort>@(_CompanionPortItems) + <_CompanionIP Condition="'$(_CompanionIP)' == ''">127.0.0.1 + <_CompanionPort Condition="'$(_CompanionPort)' == ''">11000 + + + <_TestConfigLines Include="// Auto-generated from .runsettings - do not edit directly." /> + <_TestConfigLines Include="//" /> + <_TestConfigLines Include="// Copyright (c) .NET Foundation and Contributors" /> + <_TestConfigLines Include="// See LICENSE file in the project root for full license information." /> + <_TestConfigLines Include="//" /> + <_TestConfigLines Include="namespace NFUnitTestSocketTests" /> + <_TestConfigLines Include="{" /> + <_TestConfigLines Include="%20%20%20%20internal static class TestConfiguration" /> + <_TestConfigLines Include="%20%20%20%20{" /> + <_TestConfigLines Include="%20%20%20%20%20%20%20%20internal const string CompanionIP = "$(_CompanionIP)"%3B" /> + <_TestConfigLines Include="%20%20%20%20%20%20%20%20internal const int CompanionControlPort = $(_CompanionPort)%3B" /> + <_TestConfigLines Include="%20%20%20%20}" /> + <_TestConfigLines Include="}" /> + + + + + + + + + + + diff --git a/Tests/SslStreamTests_WiFi/TestConfiguration.cs b/Tests/SslStreamTests_WiFi/TestConfiguration.cs new file mode 100644 index 0000000..4fa76d1 --- /dev/null +++ b/Tests/SslStreamTests_WiFi/TestConfiguration.cs @@ -0,0 +1,13 @@ +// Auto-generated from .runsettings - do not edit directly. +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// +namespace NFUnitTestSocketTests +{ + internal static class TestConfiguration + { + internal const string CompanionIP = "192.168.1.128"; + internal const int CompanionControlPort = 11000; + } +} diff --git a/Tests/SslStreamTests_WiFi/packages.config b/Tests/SslStreamTests_WiFi/packages.config new file mode 100644 index 0000000..4b319fb --- /dev/null +++ b/Tests/SslStreamTests_WiFi/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Tests/SslStreamTests_WiFi/packages.lock.json b/Tests/SslStreamTests_WiFi/packages.lock.json new file mode 100644 index 0000000..dd39200 --- /dev/null +++ b/Tests/SslStreamTests_WiFi/packages.lock.json @@ -0,0 +1,49 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.37, 1.11.37]", + "resolved": "1.11.37", + "contentHash": "whTgEJCGpE8eGK7Z2fI6gTqsVeVrnCzP1KDSqIB2PY0/Jyw7NcnwCFe3s/43ch75DuAZt8/jTX0VJoJtN0Colw==" + }, + "nanoFramework.System.Device.Wifi": { + "type": "Direct", + "requested": "[1.5.149, 1.5.149]", + "resolved": "1.5.149", + "contentHash": "bOceF44zpOACFYIbFTBO+Pxm8H0pg7C3AM5zzUs4wA0DiG/RB5HqKeBTUD0mpu0PTyCERsByDQMK6Pz6h1SBuw==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, + "nanoFramework.TestFramework": { + "type": "Direct", + "requested": "[3.0.80, 3.0.80]", + "resolved": "3.0.80", + "contentHash": "o2ymxAz6TC6VguKqN6rrGB/L4sVAIJVEut87Hq8MvwEe6P/7JwzX+VB4vKJxEmihCSJs7Z3Yo9Rb8HgIEo8DMg==" + } + } + } +} diff --git a/nanoFramework.System.Net.sln b/nanoFramework.System.Net.sln index e0b8384..5a50167 100644 --- a/nanoFramework.System.Net.sln +++ b/nanoFramework.System.Net.sln @@ -24,6 +24,10 @@ Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "IPAddressTests", "Tests\IPA EndProject Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "SocketTests", "Tests\SocketTests\SocketTests.nfproj", "{6233288D-091E-4795-AA1B-FD069B7F6305}" EndProject +Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "SslStreamTests_WiFi", "Tests\SslStreamTests_WiFi\SslStreamTests_WiFi.nfproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "SslStreamTests", "Tests\SslStreamTests\SslStreamTests.nfproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +58,18 @@ Global {6233288D-091E-4795-AA1B-FD069B7F6305}.Release|Any CPU.ActiveCfg = Release|Any CPU {6233288D-091E-4795-AA1B-FD069B7F6305}.Release|Any CPU.Build.0 = Release|Any CPU {6233288D-091E-4795-AA1B-FD069B7F6305}.Release|Any CPU.Deploy.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Deploy.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,6 +78,8 @@ Global {07D7468C-F619-4E73-A431-0B47D450462B} = {C9AF0EE0-09CD-4836-9881-9E62766D1CC4} {12BF8129-5D2E-446F-962F-8454DE064C0F} = {C9AF0EE0-09CD-4836-9881-9E62766D1CC4} {6233288D-091E-4795-AA1B-FD069B7F6305} = {C9AF0EE0-09CD-4836-9881-9E62766D1CC4} + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {C9AF0EE0-09CD-4836-9881-9E62766D1CC4} + {B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {C9AF0EE0-09CD-4836-9881-9E62766D1CC4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {13C81A4B-2DD5-438B-B36D-C05FBE530BC9}