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
+13
View File
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />
</ItemGroup>
</Project>
+529
View File
@@ -0,0 +1,529 @@
using AADJoin.Creds;
using AADJoin.Messages;
using AADJoin.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace AADJoin
{
internal class AzureAD {
/// <summary>
/// Checks if the account associated with provided username exists and active
/// </summary>
/// <param name="username">Account username</param>
/// <returns></returns>
public static async Task CheckUsername(string username)
{
Console.WriteLine("Start {0} username checking...", username);
var url = String.Format("https://login.microsoftonline.com/common/userrealm/{0}?api-version=1.0", username);
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine("Error. Status code: {0}. Body:", response.StatusCode);
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
Console.WriteLine("User exist and active");
}
/// <summary>
/// Performs basic authorization in the Azure AD using account username and password.
/// It's just account authorization and not related to any tenant.
/// </summary>
/// <param name="clientId">client id</param>
/// <param name="user">Account credentials</param>
/// <returns>AuthResponse object</returns>
public static async Task<AuthResponse> AzureAdAuthorize(string clientId, UserCreds user)
{
Console.WriteLine("Start Azure AD authorization...");
var url = "https://login.microsoftonline.com//common/oauth2/token";
var httpClient = new HttpClient();
var form = new Dictionary<string, string>();
form.Add("grant_type", "password");
form.Add("password", user.Password);
form.Add("client_id", clientId);
form.Add("username", user.Username);
form.Add("resource", "https://graph.windows.net");
form.Add("scope", "openid");
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(form));
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
var authResponse = JsonSerializer.Deserialize<AuthResponse>(content);
Console.WriteLine("Azure AD authorization succeeded!");
return authResponse;
}
/// <summary>
/// Performs user authorization in the tenant using previously obtained refresh token.
/// </summary>
/// <param name="clientId">client id</param>
/// <param name="authResponse">AuthResponse object returned from the AzureADAuthorize method</param>
/// <returns>TenantAuthResponse object</returns>
public static async Task<TenantAuthResponse> AzureTenantAuthorize(string clientId, AuthResponse authResponse)
{
Console.WriteLine("Start Azure Tenant authorization...");
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", authResponse.GetTenantId());
var httpClient = new HttpClient();
var form = new Dictionary<string, string>();
form.Add("scope", "openid");
form.Add("grant_type", "refresh_token");
// https://github.com/Gerenios/AADInternals/blob/master/AccessToken.ps1#L1864
// 01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9 means urn:ms-drs:enterpriseregistration.windows.net
// More UUIDs: https://www.rickvanrousselt.com/blog/azure-default-service-principals-reference-table/
form.Add("resource", "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9");
form.Add("client_id", clientId);
form.Add("refresh_token", authResponse.refresh_token);
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(form));
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
var tenantAuthResponse = TenantAuthResponse.FromString(content);
Console.WriteLine("Azure Tenant authorization succeeded!");
return tenantAuthResponse;
}
/// <summary>
/// Joins new device to the Azure AD. Automatically creates new device key/certificate, and id. The device id is
/// a randomly generated UUID. The private key password is the device id.
/// Saves them in corresponding files:
/// * `{device_id}.key` - the device private key; {device_id} is a password from the key;
/// * `{random_id}.csr` - the device certificate request that has been used to obtain the device certificate;
/// * `{device_id}.cer` - the device certificate;
/// </summary>
/// <param name="domain">Azure AD domain</param>
/// <param name="tenantAuthResponse">TenantAuthResponse object returned from the AzureTenantAuthorize method</param>
/// <returns>Device creds object</returns>
public static async Task<DeviceCreds> JoinDevice(string domain, TenantAuthResponse tenantAuthResponse)
{
Console.WriteLine("Start device joining...");
var url = "https://enterpriseregistration.windows.net/EnrollmentServer/device/?api-version=1.0";
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tenantAuthResponse.access_token);
var deviceId = Guid.NewGuid();
// minimal key size = 2048
var deviceKey = new RSACng(2048);
var joinDeviceRequest = new JoinDeviceRequest(domain, deviceKey, deviceId);
var data = new StringContent(joinDeviceRequest.ToString());
data.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await httpClient.PostAsync(url, data);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
var joinDeviceResponse = JoinDeviceResponse.FromString(content);
var certificate = new X509Certificate(Convert.FromBase64String(joinDeviceResponse.Certificate.RawBody));
var device = new DeviceCreds(deviceKey, certificate);
Console.WriteLine("The device transport key password: {0}", device.Id);
Console.WriteLine(
"The device transport key (PKCS8) has been written into the file: {0}",
CryptoUtils.RsaToPkcs8File(deviceKey, device.Id, device.Id)
);
Console.WriteLine(
"The device certificate (.pfx) has been written into the file: {0}",
CryptoUtils.PemCrtToPfxFile(joinDeviceResponse.Certificate.RawBody, deviceKey, device.Id, device.Id)
);
Console.WriteLine(
"The device certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(joinDeviceResponse.Certificate.RawBody, device.Id)
);
Console.WriteLine("Device joining succeeded!");
return device;
}
/// <summary>
/// Requests nonce from the Azure AD tenant.
/// </summary>
/// <param name="tenantId">Tenant UUID</param>
/// <returns>Base64 encoded nonce</returns>
private static async Task<string> RequestNonce(string tenantId)
{
Console.WriteLine("Start obtaining nonce...");
var httpClient = new HttpClient();
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", tenantId);
var nonceForm = new Dictionary<string, string>();
nonceForm.Add("grant_type", "srv_challenge");
var nonceResponse = await httpClient.PostAsync(url, new FormUrlEncodedContent(nonceForm));
var nonceContent = await nonceResponse.Content.ReadAsStringAsync();
if (nonceResponse.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", nonceResponse.StatusCode));
Console.WriteLine(nonceContent);
Environment.Exit(1);
}
Console.WriteLine(nonceContent);
var document = JsonDocument.Parse(nonceContent);
var nonce = document.RootElement.GetProperty("Nonce");
return nonce.ToString();
}
/// <summary>
/// Obtains refresh and access token with the TGT.
/// </summary>
/// <param name="user">Account credentials</param>
/// <param name="tenantId">Tenant id</param>
/// <param name="nonce">Previously obtained nonce</param>
/// <param name="device">Device creds (private key and certificate)</param>
/// <returns>JSON response from AzureAD as string</returns>
private static async Task<string> TokenWithTgt(
UserCreds user,
string tenantId,
string nonce,
DeviceCreds device
)
{
Console.WriteLine("Start TokenWithTGT...");
var httpClient = new HttpClient();
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", tenantId);
var certForm = new Dictionary<string, string>();
certForm.Add("request", JwtUtils.GenerateFRequestJwt(
nonce,
device,
user
));
certForm.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
certForm.Add("client_info", "1");
certForm.Add("tgt", "true");
certForm.Add("windows_api_version", "2.2");
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(certForm));
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
return content;
}
/// <summary>
/// Checks if the provided domain exists and active
/// </summary>
/// <param name="domain">AzureAD domain</param>
/// <returns></returns>
private static async Task CheckDomain(string domain)
{
var url = String.Format("https://login.microsoftonline.com/common/UserRealm/?user={0}&api-version=1.0&checkForMicrosoftAccount=false&fallback_domain={1}", domain, domain);
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine("Error. Status code: {0}. Body:", response.StatusCode);
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
Console.WriteLine("Domain exist and active");
}
/// <summary>
/// Obtains client P2P certificates ans saves them into the corresponding files.
/// </summary>
/// <param name="user">Account credentials</param>
/// <param name="tenantId">Tenant id</param>
/// <param name="device">Device credentials (private key and certificate)</param>
/// <returns></returns>
public static async Task ObtainClientP2PCertificate(UserCreds user, string tenantId, DeviceCreds device)
{
Console.WriteLine("Start obtaining client P2P certificates...");
await CheckDomain(user.Domain);
var nonce = await RequestNonce(tenantId);
Console.WriteLine("First nonce: {0}", nonce);
var response = await TokenWithTgt(user, tenantId, nonce, device);
var responseJson = JsonDocument.Parse(response);
var refreshToken = responseJson.RootElement.GetProperty("refresh_token").ToString();
var sessionKeyJwe = responseJson.RootElement.GetProperty("session_key_jwe").ToString();
var sessionKey = CryptoUtils.DecryptSessionKeyFromJwe(sessionKeyJwe, device.Key);
nonce = await RequestNonce(tenantId);
Console.WriteLine("Second nonce: {0}", nonce);
// randomly generated nonce
// we can hard code it for our tool
byte[] context = { 25, 152, 185, 126, 55, 118, 199, 221, 254, 108, 255, 202, 88, 128, 76, 218, 200, 157, 211, 63, 242, 37, 152, 198 };
var contextBase64 = "GZi5fjd2x93+bP/KWIBM2sid0z/yJZjG";
var signingKey = CryptoUtils.DeriveSigningKey(sessionKey, context);
var certificateJwe = await RdpCertificate(
nonce,
contextBase64,
tenantId,
refreshToken,
signingKey,
user,
device
);
var certificates = CryptoUtils.DecryptCeritficate(sessionKey, CryptoUtils.ExtractContextFromJwe(certificateJwe), certificateJwe);
var id = string.Format("{0}_client_auth", device.Id);
Console.WriteLine(
"The client P2P certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(certificates[0], id + "_p2p")
);
Console.WriteLine(
"The client P2P CA certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(certificates[1], id + "_p2p_ca")
);
Console.WriteLine("Finished obtaining client P2P certificates!");
}
private static async Task<string> RdpCertificate(
string nonce,
string context,
string tenantId,
string refreshToken,
byte[] signingKey,
UserCreds user,
DeviceCreds device
)
{
Console.WriteLine("Start RDP certificate obtaining...");
var httpClient = new HttpClient();
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", tenantId);
var certForm = new Dictionary<string, string>();
certForm.Add("request", JwtUtils.GenerateRdpCertificateRequestJwt(
nonce,
context,
user.Username,
refreshToken,
signingKey,
device.Key
));
certForm.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
certForm.Add("windows_api_version", "2.2");
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(certForm));
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
return content;
}
/// <summary>
/// Obtains P2P certificate for server autherization using device key and certificate. Saves certificates in the
/// corresponding files:
/// * `{device_id}_server_auth_p2p.cer`
/// * `{device_id}_server_auth_p2p_ca.cer`
/// </summary>
/// <param name="domain">AzureAD domain</param>
/// <param name="tenantId">Tenant id</param>
/// <param name="device">Device credentials</param>
/// <returns></returns>
public static async Task ObtainServerP2PCertificate(string domain, string tenantId, DeviceCreds device)
{
Console.WriteLine("Start obtaining server P2P certificates...");
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", tenantId);
var nonce = await RequestNonce(tenantId);
var certForm = new Dictionary<string, string>();
certForm.Add("request", JwtUtils.GenerateP2PRequestJwt(
nonce,
device,
string.Format("tenjo.{0}", domain)
));
certForm.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
certForm.Add("windows_api_version", "2.0");
var httpClient = new HttpClient();
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(certForm));
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
var p2pCerificatesResponse = P2PCertificatesResponse.FromString(content);
var id = string.Format("{0}_server_auth", device.Id);
Console.WriteLine(
"The server P2P certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(p2pCerificatesResponse.x5c, id + "_p2p")
);
Console.WriteLine(
"The server P2P CA certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(p2pCerificatesResponse.x5c_ca, id + "_p2p_ca")
);
Console.WriteLine("Finished obtaining server P2P certificates!");
}
/// <summary>
/// Obtains P2P certificates.
/// </summary>
/// <param name="deviceId">Device id</param>
/// <param name="domain">Azure AD domain</param>
/// <param name="tenantId">Azure AD tenant id</param>
/// <param name="device">Device credentials (private key and certificate)</param>
/// <returns></returns>
public static async Task ObtainP2PCertificates(string deviceId, string domain, string tenantId, DeviceCreds device)
{
Console.WriteLine("Start obtaining device P2P certificates...");
var httpClient = new HttpClient();
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", tenantId);
var nonceForm = new Dictionary<string, string>();
nonceForm.Add("grant_type", "srv_challenge");
var nonceResponse = await httpClient.PostAsync(url, new FormUrlEncodedContent(nonceForm));
var nonceContent = await nonceResponse.Content.ReadAsStringAsync();
if (nonceResponse.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", nonceResponse.StatusCode));
Console.WriteLine(nonceContent);
Environment.Exit(1);
}
Console.WriteLine(nonceContent);
var document = JsonDocument.Parse(nonceContent);
var nonce = document.RootElement.GetProperty("Nonce");
var certForm = new Dictionary<string, string>();
certForm.Add("request", JwtUtils.GenerateP2PRequestJwt(
nonce.ToString(),
device,
string.Format("mypc.{0}", domain)
));
certForm.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
certForm.Add("windows_api_version", "2.0");
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(certForm));
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(string.Format("Error. Status code: {0}. Body:", response.StatusCode));
Console.WriteLine(content);
Environment.Exit(1);
}
Console.WriteLine(content);
var p2pCerificatesResponse = P2PCertificatesResponse.FromString(content);
Console.WriteLine(
"The device P2P certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(p2pCerificatesResponse.x5c, deviceId + "_p2p")
);
Console.WriteLine(
"The device P2P CA certificate (.cer) has been written into the file: {0}",
CryptoUtils.PemCrtToFile(p2pCerificatesResponse.x5c_ca, deviceId + "_p2p_ca")
);
Console.WriteLine("Finished obtaining device P2P certificates!");
}
}
}
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace AADJoin.Creds
{
public class DeviceCreds
{
public string Id { get; set; }
public RSA Key { get; set; }
public X509Certificate Certificate { get; set; }
public DeviceCreds(string id, RSA key, X509Certificate certificate)
{
Id = id;
Key = key;
Certificate = certificate;
}
public DeviceCreds(RSA key, X509Certificate certificate)
{
Console.WriteLine(certificate.Subject);
Id = certificate.Subject.Split('=')[1];
Key = key;
Certificate = certificate;
}
public static DeviceCreds Empty()
{
return new DeviceCreds(null, null, null);
}
public static DeviceCreds FromPfx(string pfxPath, string password)
{
X509Certificate2 cert = new X509Certificate2(pfxPath, password, X509KeyStorageFlags.Exportable);
return new DeviceCreds(
cert.GetRSAPrivateKey(),
new X509Certificate(Convert.FromHexString(cert.GetRawCertDataString()))
);
}
}
}
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AADJoin.Creds
{
public class UserCreds
{
public string Username { get; set; }
public string Password { get; set; }
public string Domain { get; set; }
public UserCreds(string username, string password, string domain)
{
if (username == null)
{
throw new ArgumentNullException(nameof(username));
}
if (password == null)
{
throw new ArgumentNullException(nameof(password));
}
if (domain == null)
{
throw new ArgumentNullException(nameof(domain));
}
Username = username;
Password = password;
Domain = domain;
}
}
}
@@ -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);
}
}
}
+31
View File
@@ -0,0 +1,31 @@
using CommandLine;
namespace AADJoin
{
public class Options
{
[Option('j', "join-new-device", Required = false, HelpText = "Join new device to the Azure AD")]
public bool JoinNewDevice { get; set; }
[Option('c', "client-p2p-cert", Required = false, HelpText = "Obtain P2P certificate for the client authorization")]
public bool ClientP2PCert { get; set; }
[Option('s', "server-p2p-cert", Required = false, HelpText = "Obtain P2P certificate for the server authorization")]
public bool ServerP2PCert { get; set; }
[Option('d', "domain", Required = true, HelpText = "Azure AD domain")]
public string Domain { get; set; }
[Option('u', "username", Required = true, HelpText = "User Azure AD username in FQDN format")]
public string Username { get; set; }
[Option('p', "password", Required = true, HelpText = "User password")]
public string Password { get; set; }
[Option('e', "existing-device", Required = false, HelpText = "Path to the PFX file with the device key + certificate")]
public string DevicePfx { get; set; }
[Option('f', "pfx-key-password", Required = false, HelpText = "PFX file password")]
public string PfxPassword { get; set; }
}
}
+54
View File
@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using CommandLine;
using AADJoin.Creds;
namespace AADJoin
{
public class Program
{
static async Task Main(string[] args)
{
await Parser.Default.ParseArguments<Options>(args).WithParsedAsync(RunOptions);
}
static async Task RunOptions(Options opts)
{
var device = DeviceCreds.Empty();
var user = new UserCreds(opts.Username, opts.Password, opts.Domain);
// well-known client ids: https://github.com/Gerenios/AADInternals/blob/master/AccessToken_utils.ps1#L11
// graph_api: 1b730954-1685-4b74-9bfd-dac224a7b894
string clientId = "1b730954-1685-4b74-9bfd-dac224a7b894";
await AzureAD.CheckUsername(opts.Username);
var authResponse = await AzureAD.AzureAdAuthorize(clientId, user);
if (opts.JoinNewDevice)
{
var tenantAuthResponse = await AzureAD.AzureTenantAuthorize(clientId, authResponse);
device = await AzureAD.JoinDevice(opts.Domain, tenantAuthResponse);
} else if (opts.DevicePfx != null)
{
Console.WriteLine("Trying to load device PFX file...");
device = DeviceCreds.FromPfx(opts.DevicePfx, opts.PfxPassword);
} else
{
Console.WriteLine("Error: No device information provided.");
Environment.Exit(1);
}
if (opts.ServerP2PCert)
{
await AzureAD.ObtainServerP2PCertificate(user.Domain, authResponse.GetTenantId(), device);
} else if (opts.ClientP2PCert)
{
await AzureAD.ObtainClientP2PCertificate(user, authResponse.GetTenantId(), device);
} else
{
Console.WriteLine("Nothing left to do.");
}
}
}
}
@@ -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;
}
}
}