feat(core): RDP client on IronRDP

clients::rdp connects over any caller-dialled TCP stream (direct, proxy
or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM),
optionally falling back to TLS-only logon for servers without NLA such
as xrdp (credentials are passed so they log straight in). The server
certificate is pinned through a caller check on its SHA256 fingerprint
before any credentials are sent. The desktop decodes into the VNC
client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse
operations), Display Control resizes and the deactivation-reactivation
sequence run on the session thread.

IronRDP's releases need three vendored patches (vendor/README.md):
picky and sspi with their RustCrypto release-candidate pins relaxed so
they share russh's stable versions (their own suites, 402 tests, pass
against them), and ironrdp-session with two unreleased upstream bitmap
fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops
render sheared.

Verified live against xrdp (scottyhardy/docker-remote-desktop): logon,
full desktop rendering, keyboard and mouse input.
This commit is contained in:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
@@ -0,0 +1,39 @@
using System;
using System.Text.Json;
namespace AADJoin.Messages
{
public class AuthResponse
{
public string token_type { get; set; }
public string scope { get; set; }
public string expires_in { get; set; }
public string ext_expires_in { get; set; }
public string expires_on { get; set; }
public string not_before { get; set; }
public string resource { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string id_token { get; set; }
public static AuthResponse fromString(string authResponse)
{
return JsonSerializer.Deserialize<AuthResponse>(authResponse);
}
public string GetTenantId()
{
var payload = id_token.Split('.')[1];
switch (payload.Length % 4)
{
case 2: payload += "=="; break;
case 3: payload += "="; break;
}
var document = JsonDocument.Parse(Convert.FromBase64String(payload));
var idToken = document.RootElement.GetProperty("tid");
return idToken.GetString();
}
}
}
@@ -0,0 +1,91 @@
using System;
using System.Text.Json;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using AADJoin.Utils;
namespace AADJoin.Messages
{
public class JoinDeviceRequest
{
public class Csr
{
[JsonInclude]
public string Type { get; set; }
[JsonInclude]
public string Data { get; set; }
public Csr(string type, string data)
{
Type = type;
Data = data;
}
}
public class Attrs
{
[JsonInclude]
public string ReuseDevice { get; set; }
[JsonInclude]
public string ReturnClientSid { get; set; }
[JsonInclude]
public string SharedDevice { get; set; }
public Attrs(bool reuse, bool returnClientSid, bool sharedDevice)
{
ReuseDevice = reuse.ToString();
ReturnClientSid = returnClientSid.ToString();
SharedDevice = sharedDevice.ToString();
}
}
public string TransportKey { get; set; }
public int JoinType { get; set; }
public string DeviceDisplayName { get; set; }
public string OSVersion { get; set; }
[JsonInclude]
public Csr CertificateRequest { get; set; }
public string TargetDomain { get; set; }
public string DeviceType { get; set; }
[JsonInclude]
public Attrs Attributes { get; set; }
public JoinDeviceRequest(string domain, RSACng key, Guid deviceId)
{
byte[] rsaPubKeyExport = key.Key.Export(CngKeyBlobFormat.GenericPublicBlob);
TransportKey = Convert.ToBase64String(rsaPubKeyExport);
// https://github.com/Gerenios/AADInternals/blob/master/PRT_Utils.ps1#L101
// 0 = Azure AD join
// 4 = Azure AD registered
// 6 = Azure AD hybrid join
JoinType = 0;
var now = DateTime.Now;
DeviceDisplayName = string.Format(
"Test Device {0}{1}{2} {3}{4}{5}",
now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second
);
OSVersion = "10.0.22000.978";
var csr = new CertificateRequest("CN=7E980AD9-B86D-4306-9425-9AC066FB014A", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var rawCsr = Convert.ToBase64String(csr.CreateSigningRequest());
CertificateRequest = new Csr("pkcs10", rawCsr);
Console.WriteLine("The device certificate request (CSR) has been written into the file: {0}", CryptoUtils.PemCsrToFile(rawCsr, deviceId));
TargetDomain = domain;
DeviceType = "Windows";
Attributes = new Attrs(true, true, false);
}
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
}
}
@@ -0,0 +1,39 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AADJoin.Messages
{
public class JoinDeviceResponse
{
public class Cert
{
[JsonInclude]
public string Thumbprint { get; set; }
[JsonInclude]
public string RawBody { get; set; }
}
public class Usr
{
[JsonInclude]
public string Upn { get; set; }
}
public class MembershipChgs
{
[JsonInclude]
public string LocalSID { get; set; }
[JsonInclude]
public string[] AddSIDs { get; set; }
}
public Cert Certificate { get; set; }
public Usr User { get; set; }
public MembershipChgs[] MembershipChanges { get; set; }
public static JoinDeviceResponse FromString(string data)
{
return JsonSerializer.Deserialize<JoinDeviceResponse>(data);
}
}
}
@@ -0,0 +1,21 @@
using System.Text.Json;
namespace AADJoin.Messages
{
public class P2PCertificatesResponse
{
public string token_type { get; set; }
public string expires_in { get; set; }
public string ext_expires_in { get; set; }
public string expires_on { get; set; }
public string x5c { get; set; }
public string cert_token_use { get; set; }
public string x5c_ca { get; set; }
public string resource { get; set; }
public static P2PCertificatesResponse FromString(string data)
{
return JsonSerializer.Deserialize<P2PCertificatesResponse>(data);
}
}
}
@@ -0,0 +1,24 @@
using System;
using System.Text.Json;
namespace AADJoin.Messages
{
public class TenantAuthResponse
{
public string token_type { get; set; }
public string scope { get; set; }
public string expires_in { get; set; }
public string ext_expires_in { get; set; }
public string expires_on { get; set; }
public string not_before { get; set; }
public string resource { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string id_token { get; set; }
public static TenantAuthResponse FromString(string authResponse)
{
return JsonSerializer.Deserialize<TenantAuthResponse>(authResponse);
}
}
}