Skip to content

Commit

Permalink
feat: support license caching (#634)
Browse files Browse the repository at this point in the history
Adds support for license caching by storing the license obtained
from SERVER_UPGRADE_LICENSE message and sending
CLIENT_LICENSE_INFO if a license requested by the server is already
stored in the cache.

Co-authored-by: Benoît Cortier <[email protected]>
  • Loading branch information
probakowski and CBenoit authored Jan 18, 2025
1 parent a2378ef commit dd221bf
Show file tree
Hide file tree
Showing 16 changed files with 424 additions and 100 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/ironrdp-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ ironrdp = { workspace = true, features = [
"rdpsnd",
"cliprdr",
"displaycontrol",
"connector"
] }
ironrdp-cliprdr-native.workspace = true
ironrdp-rdpsnd-native.workspace = true
Expand Down Expand Up @@ -77,6 +78,7 @@ reqwest = "0.12"
url = "2.5"
raw-window-handle = "0.6.2"
ironrdp-core = { workspace = true, features = ["alloc"] }
uuid = { version = "1.11.0"}

[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = ["Win32_Foundation"] }
Expand Down
9 changes: 5 additions & 4 deletions crates/ironrdp-client/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use core::num::ParseIntError;
use core::str::FromStr;
use std::io;

use anyhow::Context as _;
use clap::clap_derive::ValueEnum;
use clap::Parser;
use core::num::ParseIntError;
use core::str::FromStr;
use ironrdp::connector::{self, Credentials};
use ironrdp::pdu::rdp::capability_sets::MajorPlatformType;
use ironrdp::pdu::rdp::client_info::PerformanceFlags;
use std::io;
use tap::prelude::*;

const DEFAULT_WIDTH: u16 = 1920;
Expand Down Expand Up @@ -316,6 +315,8 @@ impl Config {
whoami::Platform::Android => MajorPlatformType::ANDROID,
_ => MajorPlatformType::UNSPECIFIED,
},
hardware_id: None,
license_cache: None,
no_server_pointer: args.no_server_pointer,
autologon: args.autologon,
request_data: None,
Expand Down
13 changes: 9 additions & 4 deletions crates/ironrdp-connector/src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use core::mem;
use std::borrow::Cow;
use std::net::SocketAddr;

use ironrdp_core::{decode, encode_vec, Encode, WriteBuf};
use ironrdp_pdu::rdp::client_info::{OptionalSystemTime, TimezoneInfo};
use ironrdp_pdu::x224::X224;
use ironrdp_pdu::{gcc, mcs, nego, rdp, PduHint};
use ironrdp_svc::{StaticChannelSet, StaticVirtualChannel, SvcClientProcessor};
use std::borrow::Cow;
use std::net::SocketAddr;
use std::sync::Arc;

use crate::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
use crate::connection_activation::{ConnectionActivationSequence, ConnectionActivationState};
use crate::license_exchange::LicenseExchangeSequence;
use crate::license_exchange::{LicenseExchangeSequence, NoopLicenseCache};
use crate::{
encode_x224_packet, Config, ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize, Sequence, State,
Written,
Expand Down Expand Up @@ -481,6 +481,11 @@ impl Sequence for ClientConnector {
io_channel_id,
self.config.credentials.username().unwrap_or("").to_owned(),
self.config.domain.clone(),
self.config.hardware_id.unwrap_or_default(),
self.config
.license_cache
.clone()
.unwrap_or_else(|| Arc::new(NoopLicenseCache)),
),
},
),
Expand Down
12 changes: 9 additions & 3 deletions crates/ironrdp-connector/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ pub mod credssp;
mod license_exchange;
mod server_name;

use core::any::Any;
use core::fmt;

pub use crate::license_exchange::LicenseCache;
pub use channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
pub use connection::{encode_send_data_request, ClientConnector, ClientConnectorState, ConnectionResult};
pub use connection_finalization::{ConnectionFinalizationSequence, ConnectionFinalizationState};
use core::any::Any;
use core::fmt;
use ironrdp_core::{encode_buf, encode_vec, Encode, WriteBuf};
use ironrdp_pdu::nego::NegoRequestData;
use ironrdp_pdu::rdp::capability_sets;
Expand All @@ -32,6 +32,7 @@ use ironrdp_pdu::{gcc, x224, PduHint};
pub use license_exchange::{LicenseExchangeSequence, LicenseExchangeState};
pub use server_name::ServerName;
pub use sspi;
use std::sync::Arc;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
Expand Down Expand Up @@ -161,6 +162,10 @@ pub struct Config {
pub dig_product_id: String,
pub client_dir: String,
pub platform: capability_sets::MajorPlatformType,
/// Unique identifier for the computer
///
/// Each 32-bit integer contains client hardware-specific data helping the server uniquely identify the client.
pub hardware_id: Option<[u32; 4]>,
/// Optional data for the x224 connection request.
///
/// Fallbacks to a sensible default depending on the provided credentials:
Expand All @@ -170,6 +175,7 @@ pub struct Config {
pub request_data: Option<NegoRequestData>,
/// If true, the INFO_AUTOLOGON flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu)
pub autologon: bool,
pub license_cache: Option<Arc<dyn LicenseCache>>,

// FIXME(@CBenoit): these are client-only options, not part of the connector.
pub no_server_pointer: bool,
Expand Down
181 changes: 133 additions & 48 deletions crates/ironrdp-connector/src/license_exchange.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use super::{legacy, ConnectorError, ConnectorErrorExt};
use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written};
use core::fmt::Debug;
use core::{fmt, mem};

use ironrdp_core::WriteBuf;
use ironrdp_pdu::rdp::server_license::{self, LicensePdu, ServerLicenseError};
use ironrdp_pdu::rdp::server_license::{self, LicenseInformation, LicensePdu, ServerLicenseError};
use ironrdp_pdu::PduHint;
use rand_core::{OsRng, RngCore as _};

use super::legacy;
use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written};
use std::str;
use std::sync::Arc;

#[derive(Default, Debug)]
#[non_exhaustive]
Expand Down Expand Up @@ -57,15 +58,43 @@ pub struct LicenseExchangeSequence {
pub io_channel_id: u16,
pub username: String,
pub domain: Option<String>,
pub hardware_id: [u32; 4],
pub license_cache: Arc<dyn LicenseCache>,
}

pub trait LicenseCache: Sync + Send + Debug {
fn get_license(&self, license_info: LicenseInformation) -> ConnectorResult<Option<Vec<u8>>>;
fn store_license(&self, license_info: LicenseInformation) -> ConnectorResult<()>;
}

#[derive(Debug)]
pub(crate) struct NoopLicenseCache;

impl LicenseCache for NoopLicenseCache {
fn get_license(&self, _license_info: LicenseInformation) -> ConnectorResult<Option<Vec<u8>>> {
Ok(None)
}

fn store_license(&self, _license_info: LicenseInformation) -> ConnectorResult<()> {
Ok(())
}
}

impl LicenseExchangeSequence {
pub fn new(io_channel_id: u16, username: String, domain: Option<String>) -> Self {
pub fn new(
io_channel_id: u16,
username: String,
domain: Option<String>,
hardware_id: [u32; 4],
license_cache: Arc<dyn LicenseCache>,
) -> Self {
Self {
state: LicenseExchangeState::NewLicenseRequest,
io_channel_id,
username,
domain,
hardware_id,
license_cache,
}
}
}
Expand Down Expand Up @@ -107,52 +136,102 @@ impl Sequence for LicenseExchangeSequence {
let mut premaster_secret = [0u8; server_license::PREMASTER_SECRET_SIZE];
OsRng.fill_bytes(&mut premaster_secret);

match server_license::ClientNewLicenseRequest::from_server_license_request(
&license_request,
&client_random,
&premaster_secret,
&self.username,
self.domain.as_deref().unwrap_or(""),
) {
Ok((new_license_request, encryption_data)) => {
trace!(?encryption_data, "Successfully generated Client New License Request");
info!(message = ?new_license_request, "Send");

let written = encode_send_data_request::<LicensePdu>(
send_data_indication_ctx.initiator_id,
send_data_indication_ctx.channel_id,
&new_license_request.into(),
output,
)?;

(
Written::from_size(written)?,
LicenseExchangeState::PlatformChallenge { encryption_data },
)
let license_info = license_request
.scope_list
.iter()
.filter_map(|scope| {
self.license_cache
.get_license(LicenseInformation {
version: license_request.product_info.version,
scope: scope.0.clone(),
company_name: license_request.product_info.company_name.clone(),
product_id: license_request.product_info.product_id.clone(),
license_info: vec![],
})
.transpose()
})
.next()
.transpose()?;

if let Some(info) = license_info {
match server_license::ClientLicenseInfo::from_server_license_request(
&license_request,
&client_random,
&premaster_secret,
self.hardware_id,
info,
) {
Ok((client_license_info, encryption_data)) => {
trace!(?encryption_data, "Successfully generated Client License Info");
info!(message = ?client_license_info, "Send");

let written = encode_send_data_request::<LicensePdu>(
send_data_indication_ctx.initiator_id,
send_data_indication_ctx.channel_id,
&client_license_info.into(),
output,
)?;

trace!(?written, "Written ClientLicenseInfo");

(
Written::from_size(written)?,
LicenseExchangeState::PlatformChallenge { encryption_data },
)
}
Err(err) => {
return Err(custom_err!("ClientNewLicenseRequest", err));
}
}
Err(error) => {
if let ServerLicenseError::InvalidX509Certificate {
source: error,
cert_der,
} = &error
{
struct BytesHexFormatter<'a>(&'a [u8]);

impl fmt::Display for BytesHexFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x")?;
self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}"))
} else {
let hwid = self.hardware_id;
match server_license::ClientNewLicenseRequest::from_server_license_request(
&license_request,
&client_random,
&premaster_secret,
&self.username,
&format!("{:X}-{:X}-{:X}-{:X}", hwid[0], hwid[1], hwid[2], hwid[3]),
) {
Ok((new_license_request, encryption_data)) => {
trace!(?encryption_data, "Successfully generated Client New License Request");
info!(message = ?new_license_request, "Send");

let written = encode_send_data_request::<LicensePdu>(
send_data_indication_ctx.initiator_id,
send_data_indication_ctx.channel_id,
&new_license_request.into(),
output,
)?;

(
Written::from_size(written)?,
LicenseExchangeState::PlatformChallenge { encryption_data },
)
}
Err(error) => {
if let ServerLicenseError::InvalidX509Certificate {
source: error,
cert_der,
} = &error
{
struct BytesHexFormatter<'a>(&'a [u8]);

impl fmt::Display for BytesHexFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x")?;
self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}"))
}
}

error!(
%error,
cert_der = %BytesHexFormatter(cert_der),
"Unsupported or invalid X509 certificate received during license exchange step"
);
}

error!(
%error,
cert_der = %BytesHexFormatter(cert_der),
"Unsupported or invalid X509 certificate received during license exchange step"
);
return Err(custom_err!("ClientNewLicenseRequest", error));
}

return Err(custom_err!("ClientNewLicenseRequest", error));
}
}
}
Expand Down Expand Up @@ -188,7 +267,7 @@ impl Sequence for LicenseExchangeSequence {
let challenge_response =
server_license::ClientPlatformChallengeResponse::from_server_platform_challenge(
&challenge,
self.domain.as_deref().unwrap_or(""),
self.hardware_id,
&encryption_data,
)
.map_err(|e| custom_err!("ClientPlatformChallengeResponse", e))?;
Expand Down Expand Up @@ -242,6 +321,12 @@ impl Sequence for LicenseExchangeSequence {
.map_err(|e| custom_err!("license verification", e))?;

debug!("License verified with success");

let license_info = upgrade_license
.new_license_info(&encryption_data)
.map_err(ConnectorError::decode)?;

self.license_cache.store_license(license_info)?
}
LicensePdu::LicensingErrorMessage(error_message) => {
if error_message.error_code != server_license::LicenseErrorCode::StatusValidClient {
Expand Down
Loading

0 comments on commit dd221bf

Please sign in to comment.