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:
+224
@@ -0,0 +1,224 @@
|
||||
use crate::key::PublicKey;
|
||||
use crate::putty::PuttyError;
|
||||
use crate::ssh::SshPublicKey;
|
||||
use crate::ssh::decode::SshComplexTypeDecode;
|
||||
use crate::ssh::encode::SshComplexTypeEncode;
|
||||
use crate::ssh::public_key::SshBasePublicKey;
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
|
||||
|
||||
const PUTTY_PUBKEY_HEADER: &str = "---- BEGIN SSH2 PUBLIC KEY ----";
|
||||
const PUTTY_PUBKEY_FOOTER: &str = "---- END SSH2 PUBLIC KEY ----";
|
||||
|
||||
/// PuTTY public key format.
|
||||
///
|
||||
/// ### Functionality:
|
||||
/// - Conversion to/from OpenSSH format.
|
||||
/// - Encoding/decoding to/from string.
|
||||
/// - Could be extracted from [`crate::putty::Ppk`] private keys.
|
||||
///
|
||||
/// ### Notes
|
||||
/// - Although top-level containeris similar to PEM, it is not compatible with it because of
|
||||
/// additional comment field after the header.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuttyPublicKey {
|
||||
pub(crate) base: PuttyBasePublicKey,
|
||||
pub(crate) comment: String,
|
||||
}
|
||||
|
||||
impl PuttyPublicKey {
|
||||
/// Converts an OpenSSH public key to a PuTTY public key.
|
||||
pub fn from_openssh(key: &SshPublicKey) -> Result<Self, PuttyError> {
|
||||
let base = PuttyBasePublicKey::from_openssh(&key.inner_key)?;
|
||||
|
||||
Ok(Self {
|
||||
base,
|
||||
comment: key.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the key to an OpenSSH public key.
|
||||
pub fn to_openssh(&self) -> Result<SshPublicKey, PuttyError> {
|
||||
let base = self.base.to_openssh()?;
|
||||
Ok(SshPublicKey {
|
||||
inner_key: base,
|
||||
comment: self.comment.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns key comment.
|
||||
pub fn comment(&self) -> &str {
|
||||
&self.comment
|
||||
}
|
||||
|
||||
/// Returns a new public key instance with a different comment.
|
||||
pub fn with_comment(&self, comment: &str) -> Self {
|
||||
Self {
|
||||
comment: comment.to_string(),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses and returns the inner key as standard picky key type.
|
||||
pub fn to_inner_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
self.base.to_inner_key()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PuttyPublicKey {
|
||||
// False positive, clippy does not take into account that [`String::replace`] requires both
|
||||
// arguments to be the same type, e.g. we can't use `&str` as a replacement for `char`.
|
||||
#[allow(clippy::single_char_pattern)]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
const MAX_CHARS_PER_LINE: usize = 64;
|
||||
const LINE_END: &str = "\r\n";
|
||||
|
||||
let encoded_key = BASE64_ENGINE.encode(&self.base.data);
|
||||
let escaped_comment = self.comment.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
|
||||
let mut output = String::new();
|
||||
output.push_str(PUTTY_PUBKEY_HEADER);
|
||||
output.push_str(LINE_END);
|
||||
output.push_str("Comment: \"");
|
||||
output.push_str(&escaped_comment);
|
||||
output.push('"');
|
||||
output.push_str(LINE_END);
|
||||
|
||||
let mut value_remaining = encoded_key.as_str();
|
||||
|
||||
while !value_remaining.is_empty() {
|
||||
let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE);
|
||||
let (line, remaining) = value_remaining.split_at(line_len);
|
||||
value_remaining = remaining;
|
||||
|
||||
output.push_str(line);
|
||||
output.push_str(LINE_END);
|
||||
}
|
||||
|
||||
output.push_str(PUTTY_PUBKEY_FOOTER);
|
||||
output.push_str(LINE_END);
|
||||
|
||||
f.write_str(&output)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PuttyPublicKey {
|
||||
type Err = PuttyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut lines = s.lines();
|
||||
let header = lines.next().ok_or(PuttyError::EndOfInput)?;
|
||||
if header != PUTTY_PUBKEY_HEADER {
|
||||
return Err(PuttyError::InvalidPublicKeyContainer);
|
||||
}
|
||||
|
||||
let comment_line = lines.next().ok_or(PuttyError::EndOfInput)?;
|
||||
if !comment_line.starts_with("Comment: ") {
|
||||
return Err(PuttyError::InvalidPublicKeyComment);
|
||||
}
|
||||
|
||||
let unescaped_comment = comment_line
|
||||
.split_once('"')
|
||||
.and_then(|(_, remainder)| remainder.rsplit_once('"'))
|
||||
.map(|(comment, _)| comment)
|
||||
.ok_or(PuttyError::InvalidPublicKeyComment)?;
|
||||
|
||||
let comment = unescaped_comment.replace("\\\"", "\"").replace("\\\\", "\\");
|
||||
|
||||
let mut encoded_key = String::new();
|
||||
for line in lines {
|
||||
if line == PUTTY_PUBKEY_FOOTER {
|
||||
let decoded = BASE64_ENGINE
|
||||
.decode(encoded_key)
|
||||
.map_err(|_| PuttyError::InvalidPublicKeyData)?;
|
||||
|
||||
return Ok(PuttyPublicKey {
|
||||
base: PuttyBasePublicKey { data: decoded },
|
||||
comment,
|
||||
});
|
||||
}
|
||||
|
||||
encoded_key.push_str(line);
|
||||
}
|
||||
|
||||
Err(PuttyError::EndOfInput)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PuttyBasePublicKey {
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PuttyBasePublicKey {
|
||||
pub fn from_openssh(key: &SshBasePublicKey) -> Result<Self, PuttyError> {
|
||||
match key {
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => {
|
||||
// Putty does not support SK keys
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut data = Vec::new();
|
||||
SshBasePublicKey::encode(key, &mut data)?;
|
||||
|
||||
Ok(Self { data })
|
||||
}
|
||||
|
||||
pub fn to_openssh(&self) -> Result<SshBasePublicKey, PuttyError> {
|
||||
let key = SshBasePublicKey::decode(self.data.as_slice())?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn to_inner_key(&self) -> Result<PublicKey, PuttyError> {
|
||||
let inner = match self.to_openssh()? {
|
||||
SshBasePublicKey::Rsa(key) => key,
|
||||
SshBasePublicKey::Ec(key) => key,
|
||||
SshBasePublicKey::Ed(key) => key,
|
||||
SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => {
|
||||
return Err(PuttyError::NotSupported { feature: "SK keys" });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use picky_test_data::{
|
||||
PUTTY_KEY_ED25519_PUBLIC, PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT, PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT,
|
||||
SSH_PUBLIC_KEY_EC_P256, SSH_PUBLIC_KEY_EC_P384, SSH_PUBLIC_KEY_EC_P521, SSH_PUBLIC_KEY_ED25519,
|
||||
SSH_PUBLIC_KEY_RSA,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(PUTTY_KEY_ED25519_PUBLIC)]
|
||||
#[case(PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT)]
|
||||
#[case(PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT)]
|
||||
fn public_key_rountrip(#[case] input: &str) {
|
||||
let key: PuttyPublicKey = input.parse().unwrap();
|
||||
let output = key.to_string();
|
||||
assert_eq!(input, output);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(SSH_PUBLIC_KEY_RSA)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P256)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P384)]
|
||||
#[case(SSH_PUBLIC_KEY_EC_P521)]
|
||||
#[case(SSH_PUBLIC_KEY_ED25519)]
|
||||
fn ssh_key_roundtrip(#[case] input: &str) {
|
||||
let ssh_key: SshPublicKey = input.parse().unwrap();
|
||||
let key = PuttyPublicKey::from_openssh(&ssh_key).unwrap();
|
||||
let ssh_key2 = key.to_openssh().unwrap();
|
||||
|
||||
let ssh_key_str = ssh_key2.to_string().unwrap();
|
||||
assert_eq!(ssh_key_str, input);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user