Skip to content

Commit eb73a21

Browse files
committed
Add unit tests for SSL/TLS
- Add tests to connect to HTTPS hosts. - Update companion app to allow testing secure server and certs.
1 parent de70dce commit eb73a21

16 files changed

Lines changed: 1789 additions & 5 deletions

‎.runsettings‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<!-- Configurations that affect the Test Framework -->
44
<RunConfiguration>
55
<ResultsDirectory>.\TestResults</ResultsDirectory><!-- Path relative to solution directory -->
6-
<TestSessionTimeout>60000</TestSessionTimeout><!-- Milliseconds -->
6+
<TestSessionTimeout>90000</TestSessionTimeout><!-- Milliseconds -->
77
<TargetFrameworkVersion>net48</TargetFrameworkVersion>
88
<TargetPlatform>x64</TargetPlatform>
99
</RunConfiguration>

‎Tests/NetworkTestCompanion/CommandServer.cs‎

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Net;
5+
using System.Net.Security;
56
using System.Net.Sockets;
7+
using System.Security.Authentication;
68
using System.Text;
79
using System.Text.Json;
810
using System.Text.Json.Nodes;
@@ -20,6 +22,9 @@ namespace NetworkTestCompanion;
2022
/// { "cmd": "stop", "port": N }
2123
/// { "cmd": "stop_all" }
2224
/// { "cmd": "connect_to", "host": "...", "port": N }
25+
/// { "cmd": "start_tls_echo", "port": N }
26+
/// { "cmd": "tls_connect_to", "host": "...", "port": N }
27+
/// { "cmd": "tls_connect_echo", "host": "...", "port": N, "data": "base64..." }
2328
/// </summary>
2429
internal sealed class CommandServer : IDisposable
2530
{
@@ -112,6 +117,9 @@ private string ProcessCommand(string json)
112117
"stop" => Stop(node),
113118
"stop_all" => StopAll(),
114119
"connect_to" => ConnectTo(node),
120+
"start_tls_echo" => StartTlsEcho(node),
121+
"tls_connect_to" => TlsConnectTo(node),
122+
"tls_connect_echo" => TlsConnectEcho(node),
115123
_ => Error($"unknown command: {cmd}")
116124
};
117125
}
@@ -237,6 +245,179 @@ private string ConnectTo(JsonNode node)
237245
}
238246
}
239247

248+
private string StartTlsEcho(JsonNode node)
249+
{
250+
if (!TryGetPort(node, out var port, out var err)) return err!;
251+
252+
lock (_lock)
253+
{
254+
// Replace any stale server left registered on this port by a previous
255+
// (possibly aborted) test run — the companion is long-lived across runs.
256+
if (_activeServers.TryGetValue(port, out var existing))
257+
{
258+
existing.Dispose();
259+
_activeServers.Remove(port);
260+
Console.WriteLine($"[CMD] Replaced stale server on port {port}");
261+
}
262+
263+
var server = new TlsEchoServer(_bindAddress, port, TestCertificates.ServerCert);
264+
try
265+
{
266+
server.Start();
267+
}
268+
catch (Exception ex)
269+
{
270+
server.Dispose();
271+
return Error(ex.Message);
272+
}
273+
274+
_activeServers[port] = server;
275+
}
276+
277+
Console.WriteLine($"[CMD] TLS echo started on port {port}");
278+
return Ok();
279+
}
280+
281+
private string TlsConnectTo(JsonNode node)
282+
{
283+
var host = node["host"]?.GetValue<string>();
284+
if (string.IsNullOrEmpty(host)) return Error("missing 'host'");
285+
if (!TryGetPort(node, out var port, out var err)) return err!;
286+
287+
TcpClient? connectClient = null;
288+
try
289+
{
290+
connectClient = new TcpClient();
291+
if (!connectClient.ConnectAsync(host, port).Wait(TimeSpan.FromSeconds(5)))
292+
{
293+
connectClient.Dispose();
294+
return Error($"connect to {host}:{port} timed out");
295+
}
296+
297+
Console.WriteLine($"[CMD] tls_connect_to {host}:{port} TCP connected, starting TLS handshake in background");
298+
299+
// TLS handshake + keep-alive runs in background so the device
300+
// can call Accept() and AuthenticateAsServer() after getting Ok.
301+
var clientForBg = connectClient;
302+
_ = Task.Run(async () =>
303+
{
304+
try
305+
{
306+
var sslStream = new SslStream(
307+
clientForBg.GetStream(),
308+
leaveInnerStreamOpen: false,
309+
(_, _, _, _) => true);
310+
311+
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
312+
{
313+
TargetHost = host,
314+
EnabledSslProtocols = SslProtocols.Tls12
315+
});
316+
317+
Console.WriteLine($"[CMD] tls_connect_to {host}:{port} TLS handshake succeeded");
318+
319+
await Task.Delay(2000);
320+
sslStream.Dispose();
321+
clientForBg.Dispose();
322+
}
323+
catch (Exception ex)
324+
{
325+
Console.Error.WriteLine($"[CMD] tls_connect_to {host}:{port} TLS failed: {ex.Message}");
326+
clientForBg.Dispose();
327+
}
328+
});
329+
330+
return Ok();
331+
}
332+
catch (Exception ex)
333+
{
334+
connectClient?.Dispose();
335+
Console.Error.WriteLine($"[CMD] tls_connect_to {host}:{port} failed: {ex.Message}");
336+
return Error(ex.Message);
337+
}
338+
}
339+
340+
private string TlsConnectEcho(JsonNode node)
341+
{
342+
var host = node["host"]?.GetValue<string>();
343+
if (string.IsNullOrEmpty(host)) return Error("missing 'host'");
344+
if (!TryGetPort(node, out var port, out var err)) return err!;
345+
var dataB64 = node["data"]?.GetValue<string>();
346+
if (string.IsNullOrEmpty(dataB64)) return Error("missing 'data'");
347+
348+
byte[] dataToSend;
349+
try { dataToSend = Convert.FromBase64String(dataB64); }
350+
catch { return Error("'data' is not valid base64"); }
351+
352+
TcpClient? connectClient = null;
353+
try
354+
{
355+
connectClient = new TcpClient();
356+
if (!connectClient.ConnectAsync(host, port).Wait(TimeSpan.FromSeconds(5)))
357+
{
358+
connectClient.Dispose();
359+
return Error($"connect to {host}:{port} timed out");
360+
}
361+
362+
Console.WriteLine($"[CMD] tls_connect_echo {host}:{port} TCP connected, starting TLS + echo in background");
363+
364+
// TLS handshake + echo runs in background so the device
365+
// can call Accept() and AuthenticateAsServer() after getting Ok.
366+
var clientForBg = connectClient;
367+
_ = Task.Run(async () =>
368+
{
369+
try
370+
{
371+
var sslStream = new SslStream(
372+
clientForBg.GetStream(),
373+
leaveInnerStreamOpen: false,
374+
(_, _, _, _) => true);
375+
376+
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
377+
{
378+
TargetHost = host,
379+
EnabledSslProtocols = SslProtocols.Tls12
380+
});
381+
382+
await sslStream.WriteAsync(dataToSend);
383+
await sslStream.FlushAsync();
384+
385+
Console.WriteLine($"[CMD] tls_connect_echo {host}:{port}: sent {dataToSend.Length} bytes, waiting for echo");
386+
387+
var buf = new byte[4096];
388+
int totalRead = 0;
389+
using var ms = new MemoryStream();
390+
391+
while (totalRead < dataToSend.Length)
392+
{
393+
int read = await sslStream.ReadAsync(buf);
394+
if (read == 0) break;
395+
ms.Write(buf, 0, read);
396+
totalRead += read;
397+
}
398+
399+
Console.WriteLine($"[CMD] tls_connect_echo {host}:{port}: received {totalRead} bytes echo");
400+
401+
sslStream.Dispose();
402+
clientForBg.Dispose();
403+
}
404+
catch (Exception ex)
405+
{
406+
Console.Error.WriteLine($"[CMD] tls_connect_echo {host}:{port} TLS/echo failed: {ex.Message}");
407+
clientForBg.Dispose();
408+
}
409+
});
410+
411+
return Ok();
412+
}
413+
catch (Exception ex)
414+
{
415+
connectClient?.Dispose();
416+
Console.Error.WriteLine($"[CMD] tls_connect_echo {host}:{port} failed: {ex.Message}");
417+
return Error(ex.Message);
418+
}
419+
}
420+
240421
private static bool TryGetPort(JsonNode node, out int port, out string? error)
241422
{
242423
port = 0;

‎Tests/NetworkTestCompanion/Program.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using NetworkTestCompanion;
88

99
const int DefaultControlPort = 11000;
10-
int[] WellKnownTcpPorts = [DefaultControlPort, 7, 8, 9, 10, 80, 8080];
10+
int[] WellKnownTcpPorts = [DefaultControlPort, 7, 8, 9, 10, 80, 8080, 7010, 7011, 7012, 7013, 7014];
1111
int[] WellKnownUdpPorts = [7, 8, 9];
1212

1313
// Argument parsing
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace NetworkTestCompanion;
7+
8+
/// <summary>
9+
/// Provides the static test certificate used by the companion's TLS echo server.
10+
///
11+
/// This is a self-signed RSA-2048 test certificate (CN=nanoFramework Test Server).
12+
/// The device connects to the companion's TLS echo server with certificate
13+
/// verification disabled, so the certificate only needs to be valid enough for
14+
/// the Windows SChannel server side to complete a handshake.
15+
///
16+
/// The SAME certificate and key are embedded on the device side (SslServerTests.cs)
17+
/// for the reverse scenario (device acting as TLS server). Not a secret — a
18+
/// throw-away test certificate.
19+
/// </summary>
20+
internal static class TestCertificates
21+
{
22+
private static readonly Lazy<X509Certificate2> _serverCert = new(LoadServerCertificate);
23+
24+
internal static X509Certificate2 ServerCert => _serverCert.Value;
25+
26+
private static X509Certificate2 LoadServerCertificate()
27+
{
28+
// Load cert + key from PEM, then re-import via PFX so Windows SChannel
29+
// can use the private key (SChannel rejects the ephemeral key handle
30+
// that CreateFromPem produces).
31+
using var fromPem = X509Certificate2.CreateFromPem(ServerCertPem, ServerKeyPem);
32+
var pfxBytes = fromPem.Export(X509ContentType.Pfx);
33+
var cert = X509CertificateLoader.LoadPkcs12(pfxBytes, null, X509KeyStorageFlags.Exportable);
34+
35+
Console.WriteLine("[CERTS] Loaded static test server certificate");
36+
return cert;
37+
}
38+
39+
// Self-signed RSA-2048 test certificate, CN=nanoFramework Test Server.
40+
// Valid 2026..2036. Throw-away test cert — not a secret.
41+
internal const string ServerCertPem =
42+
@"-----BEGIN CERTIFICATE-----
43+
MIIDRTCCAi2gAwIBAgIUepRBLWtpFLvLv6rjIIUQxmkVkYkwDQYJKoZIhvcNAQEL
44+
BQAwJDEiMCAGA1UEAwwZbmFub0ZyYW1ld29yayBUZXN0IFNlcnZlcjAeFw0yNjA3
45+
MTYxMjA0MDJaFw0zNjA3MTMxMjA0MDJaMCQxIjAgBgNVBAMMGW5hbm9GcmFtZXdv
46+
cmsgVGVzdCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu
47+
+yM+X9ZcaawdwfhpJiWa4qlrA/1aV0CoENchMP6XOr4Eq7h/Y8jH+QlKdG2hFe31
48+
wULiwLJq6QwTQ23a7vRFBgTZCZJSs5QY54o2r7O6pO37Y1w+/d0/4blFLNWd0PQq
49+
Mm8TUKdK3J11dv+n/oY9++4vFHR6Bo3xjHFBvm03vcKETeF3UIX+g6J84lfNmdPs
50+
A3UIFqkWXioC7a2+afnRczAHrrS0Py2KcSv+G5E94ZYQHs0VljY8CpOEV2maxh9S
51+
Bjocv4o6HUejKoWvbXqkftuxztjYx77p++jhICpnNZjpNOb27rJhtGw3HPwtn8IY
52+
I3jIZS72insBEQgKSxBhAgMBAAGjbzBtMB0GA1UdDgQWBBR7BiZTDR7gx4gl2fV1
53+
Y9fjrb1mMjAfBgNVHSMEGDAWgBR7BiZTDR7gx4gl2fV1Y9fjrb1mMjAJBgNVHRME
54+
AjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B
55+
AQsFAAOCAQEAIeCjTx10sJiacjzajLUJ/dfgSq8VfMUhHQLxG5VF3bPMVbr8LaF+
56+
sM77OKXOBKigDj65urMUwAoLKXF7UcewvVV239Og1p97acetDDiM3Q72duH8MFAF
57+
K2V9qR6Cj8/jvQdSHWiJVOHE0u31PG6Xwa6aIwXA79VKbJaucvj9hXYz0O3HXTRc
58+
RmvHemTge0p0VhuTL+wNv46mftEZhoSsPZa4S08nv5VX3EyWfQM6eX3ghnq6CsE+
59+
MQw94r5CV1kZDA/R9IdXh4aRIVCyN0ZmMvfNNrmIJRdr/eLzQc/6DBeh5Wrg7Sc/
60+
9oUAfyIiPqXG3sW0Txof2L5qbTmallrooQ==
61+
-----END CERTIFICATE-----";
62+
63+
// PKCS#1 (traditional RSA) encoding — kept identical to the device-side key
64+
// (SslServerTests.cs), which requires this format for mbedTLS SecureServerInit.
65+
internal const string ServerKeyPem =
66+
@"-----BEGIN RSA PRIVATE KEY-----
67+
MIIEpAIBAAKCAQEArvsjPl/WXGmsHcH4aSYlmuKpawP9WldAqBDXITD+lzq+BKu4
68+
f2PIx/kJSnRtoRXt9cFC4sCyaukME0Nt2u70RQYE2QmSUrOUGOeKNq+zuqTt+2Nc
69+
Pv3dP+G5RSzVndD0KjJvE1CnStyddXb/p/6GPfvuLxR0egaN8YxxQb5tN73ChE3h
70+
d1CF/oOifOJXzZnT7AN1CBapFl4qAu2tvmn50XMwB660tD8tinEr/huRPeGWEB7N
71+
FZY2PAqThFdpmsYfUgY6HL+KOh1HoyqFr216pH7bsc7Y2Me+6fvo4SAqZzWY6TTm
72+
9u6yYbRsNxz8LZ/CGCN4yGUu9op7AREICksQYQIDAQABAoIBAB8L2Bj9EB+dcDhn
73+
bhfZ+NoeVUjzkEQzLvmi40i0VLeoaIaToUyY+8rfWNKpDbqDFZGBFMj+v6lQaCAS
74+
2q75rsWAZ+PKWvfpfOFeU5uYWR9InCD6ZCeZC2SGPEUVy2EQ7gF+qU6YBNa3hgiN
75+
cJbyBgeBZ6Vaz7/G4fB1prKvgtlcunjtXAwdme9nkHR2kuG+pGGtNs/qc71bQeOt
76+
5gphHdls+lHX+D6QD/gB2biR1bSJW+Cegz0zNM0nUUz6cA3K9dUTMeyGyI4c8FNt
77+
u/FuRZlri/I7yUAPngVnmq46rJg1Ih0OvXI+jIEHp7SI0e9MiRLnZhB4x3/76940
78+
qAJ+nnECgYEA5692QqZ0eovLlqlWSrRNP0mG31HxCTrWYvfvfWkfhGPskdsJoOeQ
79+
RXk5Mvfp84miIezv8aXxmQEquxQiP8HRtzQ0TrsvKdlM9XOGwUJULyqtFfSs6vmj
80+
HTD16GGPxWqEy0xAcaQcM31pb8YwuZ5MCNRdhuL4ladWBfMR0z75GssCgYEAwVg8
81+
hPsX+H4LzCfu32MQCMh+sAR+1xPTLSG3ydJFbx6PE9pX408ZJKc9CtkG8Xz29+y1
82+
EZnymB1IUpNZxms/4pybyFaKXUa20SpPgSoIrBaL/wdgM4h4Pvs2FSceYN9qY7Z5
83+
d4DhJEiuez+CAFVqQnrNaLJI7xD094SEv91nQAMCgYA+rq8dOzG6UgYj3e61yXA4
84+
1ijCVMYUzDFil1fZI07en7ZKg+tn+B6FXVXHX2GRfUQ7T4Jfa5kg3zrzYHAftc2K
85+
dnpMbsJE3UDAC6CCuvJRzIcFsKvz6tRhunRdib+/FqGU6y1oUZE7sQuMrR9TqOtD
86+
XEltjAzbWGmitG+3Kot03wKBgQCunriaCgWOQpj5HB/b1aaHqDzzUDwWmCskGc3a
87+
E3TudRUYAx1ZiPjWZ8zz3SsuM4UCSeEHMpkt1VSab8anM/oQ+wyflbmFoPZAVwxT
88+
RdlrQznRbaHvKRQhHdWsqRYAvAdkY0u1KMsucA5V9fe9wWck/7BBHLROZmw4mJEk
89+
kBxObQKBgQCCrcUNFhm3dxCdi+VgMwrhMOqiO5XYAGw4raQ/BbXwpb1PLbj4xtSZ
90+
WVb3utTBP0WPhf58EcHc8ko4B+1xCMR4B9rntQACfngbUN4wETQ1Gz+bNgaHQyJo
91+
gHYA38gnEOJurr2VLZFaqLgwj+7kpTRL2a0ZTDz8pCxlwDpsi4n1YA==
92+
-----END RSA PRIVATE KEY-----";
93+
}

0 commit comments

Comments
 (0)