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
+118
View File
@@ -0,0 +1,118 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.IO;
using System.Text;
using AADJoin.Creds;
namespace AADJoin.Utils
{
public class JwtUtils
{
// The AAD cloud ap identifier. it will always be "38aa3b87-a06d-4817-b275–7a316988d93b"
static string AadApIdentifier = "38aa3b87-a06d-4817-b275-7a316988d93b";
public static string GenerateRdpCertificateRequestJwt(
string nonce,
string context,
string username,
string refreshToken,
byte[] signingKey,
RSA key
)
{
var header = new JwtHeader();
header.Add("alg", "HS256");
header.Add("kdf_ver", "2");
header.Add("ctx", context);
var payload = new JwtPayload();
payload.Add("iss", "aad:brokerplugin");
payload.Add("grant_type", "refresh_token");
payload.Add("aud", "login.microsoftonline.com");
payload.Add("request_nonce", nonce);
payload.Add("scope", "openid aza ugs");
payload.Add("refresh_token", refreshToken);
payload.Add("client_id", AadApIdentifier);
payload.Add("cert_token_use", "user_cert");
payload.Add("csr_type", "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10");
var csr = new CertificateRequest(string.Format("CN = {0}", username), key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var rawCsr = Convert.ToBase64String(csr.CreateSigningRequest());
payload.Add("csr", rawCsr);
payload.Add("win_ver", "10.0.22000.653");
var dataToSign = string.Format("{0}.{1}", header.Base64UrlEncode(), payload.Base64UrlEncode());
HMACSHA256 hmac = new HMACSHA256(signingKey);
byte[] hmacOutput = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
var finalJwt = string.Format("{0}.{1}", dataToSign, Convert.ToBase64String(hmacOutput).Split('=')[0]);
Console.WriteLine(finalJwt);
return finalJwt;
}
public static string GenerateFRequestJwt(string nonce, DeviceCreds device, UserCreds user)
{
var header = new JwtHeader();
header.Add("alg", "RS256");
header.Add("typ", "JWT");
header.Add("x5c", Convert.ToBase64String(device.Certificate.Export(X509ContentType.Cert)));
header.Add("kdf_ver", "2");
var payload = new JwtPayload();
payload.Add("client_id", AadApIdentifier);
payload.Add("request_nonce", nonce);
payload.Add("scope", "openid aza ugs");
payload.Add("win_ver", "10.0.22000.653");
payload.Add("grant_type", "password");
payload.Add("username", user.Username);
payload.Add("password", user.Password);
var dataToSign = string.Format("{0}.{1}", header.Base64UrlEncode(), payload.Base64UrlEncode());
var signature = device.Key.SignData(Encoding.ASCII.GetBytes(dataToSign), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var finalJwt = string.Format("{0}.{1}", dataToSign, Convert.ToBase64String(signature).Split('=')[0]);
Console.WriteLine(finalJwt);
return finalJwt;
}
public static string GenerateP2PRequestJwt(string nonce, DeviceCreds device, string dnsName)
{
var header = new JwtHeader();
header.Add("alg", "RS256");
header.Add("typ", "JWT");
header.Add("x5c", Convert.ToBase64String(device.Certificate.Export(X509ContentType.Cert)));
var payload = new JwtPayload();
payload.Add("client_id", AadApIdentifier);
payload.Add("request_nonce", nonce);
payload.Add("win_ver", "10.0.18363.0");
payload.Add("grant_type", "device_auth");
payload.Add("cert_token_use", "device_cert");
payload.Add("csr_type", "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10");
var csr = new CertificateRequest(device.Certificate.Subject, device.Key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var rawCsr = Convert.ToBase64String(csr.CreateSigningRequest());
payload.Add("csr", rawCsr);
payload.Add("netbios_name", dnsName);
payload.Add("dns_names", new string[] { dnsName });
var dataToSign = string.Format("{0}.{1}", header.Base64UrlEncode(), payload.Base64UrlEncode());
var signature = device.Key.SignData(Encoding.ASCII.GetBytes(dataToSign), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var finalJwt = string.Format("{0}.{1}", dataToSign, Convert.ToBase64String(signature).Split('=')[0]);
Console.WriteLine(finalJwt);
return finalJwt;
}
}
}