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}