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,172 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
namespace AADJoin.Utils
{
public class CryptoUtils
{
public static string PemCsrToFile(string rawCsr, Guid id)
{
return PemCsrToFile(rawCsr, id.ToString());
}
public static string PemCsrToFile(string rawCsr, string id)
{
var fileId = id + ".csr";
string[] lines = { "-----BEGIN CERTIFICATE REQUEST-----", rawCsr, "-----END CERTIFICATE REQUEST-----" };
File.WriteAllLines(fileId, lines);
return fileId;
}
public static string PemCrtToFile(string rawCrt, Guid id)
{
return PemCrtToFile(rawCrt, id.ToString());
}
public static string PemCrtToFile(string rawCrt, string id)
{
var fileId = id + ".cer";
string[] lines = { "-----BEGIN CERTIFICATE-----", rawCrt, "-----END CERTIFICATE-----" };
File.WriteAllLines(fileId, lines);
return fileId;
}
public static string RsaToPkcs8String(RSACng rsaKey, string password)
{
var bytes = rsaKey.ExportEncryptedPkcs8PrivateKey(
password,
new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 15)
);
return Convert.ToBase64String(bytes);
}
public static string RsaToPkcs8File(RSACng rsaKey, string password, string id)
{
var fileId = id + ".key";
var bytes = rsaKey.ExportEncryptedPkcs8PrivateKey(
password,
new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 15)
);
string[] lines = {
"-----BEGIN ENCRYPTED PRIVATE KEY-----",
Convert.ToBase64String(bytes),
"-----END ENCRYPTED PRIVATE KEY-----"
};
File.WriteAllLines(fileId, lines);
return fileId;
}
public static string PemCrtToPfxFile(string rawCrt, RSA privateKey, string id, string password)
{
var fileId = id + ".pfx";
X509Certificate2 cert = new X509Certificate2(Convert.FromBase64String(rawCrt));
X509Certificate2 pfxCert = cert.CopyWithPrivateKey(privateKey);
byte[] pkcs12 = pfxCert.Export(X509ContentType.Pfx, password);
File.WriteAllBytes(fileId, pkcs12);
return fileId;
}
public static RSA Pkcs8ToRsa(string password, string content)
{
var rsa = RSA.Create();
int bytesRead = 0;
ReadOnlySpan<char> passwordSpan = password.AsSpan();
ReadOnlySpan<byte> contentSpan = Convert.FromBase64String(content);
rsa.ImportEncryptedPkcs8PrivateKey(passwordSpan, contentSpan, out bytesRead);
return rsa;
}
public static byte[] FromBase64(string payload)
{
var data = payload.Replace("_", "/").Replace("-","+");
while (data.Length % 4 != 0)
{
data += '=';
}
return Convert.FromBase64String(data);
}
public static byte[] DecryptSessionKeyFromJwe(string jwe, RSA key)
{
Console.WriteLine(jwe);
var parsedJwe = jwe.Split('.');
var cipher = FromBase64(parsedJwe[1]);
Console.WriteLine(cipher.Length);
return key.Decrypt(cipher, RSAEncryptionPadding.OaepSHA1);
}
public static byte[] ExtractContextFromJwe(string jwe)
{
var document = JsonDocument.Parse(FromBase64(jwe.Split('.')[0]));
return FromBase64(document.RootElement.GetProperty("ctx").ToString());
}
public static byte[] DeriveSigningKey(byte[] sessionKey, byte[] context)
{
byte[] label = Encoding.UTF8.GetBytes("AzureAD-SecureConversation");
List<byte> computeValue = new List<byte>();
computeValue.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x01 });
computeValue.AddRange(label);
computeValue.Add(0x00);
computeValue.AddRange(context);
computeValue.AddRange(new byte[] { 0x00, 0x00, 0x01, 0x00 });
HMACSHA256 hmac = new HMACSHA256(sessionKey);
byte[] hmacOutput = hmac.ComputeHash(computeValue.ToArray());
return hmacOutput;
}
public static string[] DecryptCeritficate(byte[] sessionKey, byte[] context, string jwe)
{
var parsedJwe = jwe.Split('.');
var iv = FromBase64(parsedJwe[2]);
var encData = FromBase64(parsedJwe[3]);
var signingKey = DeriveSigningKey(sessionKey, context);
var cryptoProvider = new AesCryptoServiceProvider();
cryptoProvider.IV = iv;
cryptoProvider.Key = signingKey;
var buffer = new MemoryStream();
var cryptoStream = new CryptoStream(buffer, cryptoProvider.CreateDecryptor(signingKey, iv), CryptoStreamMode.Write);
cryptoStream.Write(encData, 0, encData.Count());
cryptoStream.FlushFinalBlock();
var decData = buffer.ToArray();
cryptoStream.Dispose();
cryptoProvider.Dispose();
var document = JsonDocument.Parse(decData);
var rdp = document.RootElement.GetProperty("x5c").ToString();
var rdp_ca = document.RootElement.GetProperty("x5c_ca").ToString();
return new[] { rdp, rdp_ca };
}
}
}
+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;
}
}
}