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:
+269
@@ -0,0 +1,269 @@
|
||||
use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL};
|
||||
use picky_krb::crypto::CipherSuite;
|
||||
|
||||
use crate::kerberos::{EncryptionParams, KerberosConfig, KerberosState, test_data};
|
||||
use crate::{EncryptionFlags, Kerberos, SecurityBufferFlags, SecurityBufferRef, Sspi};
|
||||
|
||||
#[test]
|
||||
fn stream_buffer_decryption() {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi
|
||||
|
||||
let mut kerberos_server = test_data::fake_server();
|
||||
let mut kerberos_client = test_data::fake_client();
|
||||
|
||||
let plain_message = b"some plain message";
|
||||
|
||||
let mut token = [0; 1024];
|
||||
let mut data = plain_message.to_vec();
|
||||
let mut message = [
|
||||
SecurityBufferRef::token_buf(token.as_mut_slice()),
|
||||
SecurityBufferRef::data_buf(data.as_mut_slice()),
|
||||
];
|
||||
|
||||
kerberos_server
|
||||
.encrypt_message(EncryptionFlags::empty(), &mut message)
|
||||
.unwrap();
|
||||
|
||||
let mut buffer = message[0].data().to_vec();
|
||||
buffer.extend_from_slice(message[1].data());
|
||||
|
||||
let mut message = [
|
||||
SecurityBufferRef::stream_buf(&mut buffer),
|
||||
SecurityBufferRef::data_buf(&mut []),
|
||||
];
|
||||
|
||||
kerberos_client.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(message[1].data(), plain_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secbuffer_readonly_with_checksum() {
|
||||
// All values in this test (session keys, sequence number, encrypted and decrypted data) were extracted
|
||||
// from the original Windows Kerberos implementation calls.
|
||||
// We keep this test to guarantee full compatibility with the original Kerberos.
|
||||
|
||||
let session_key = [
|
||||
114, 67, 55, 26, 76, 210, 61, 0, 164, 44, 11, 133, 108, 220, 234, 145, 61, 144, 123, 45, 54, 175, 164, 168, 99,
|
||||
18, 99, 240, 242, 157, 95, 134,
|
||||
];
|
||||
let sub_session_key = [
|
||||
91, 11, 188, 227, 10, 91, 180, 246, 64, 129, 251, 200, 118, 82, 109, 65, 241, 177, 109, 32, 124, 39, 127, 171,
|
||||
222, 132, 199, 199, 126, 110, 3, 166,
|
||||
];
|
||||
|
||||
let mut kerberos_server = Kerberos {
|
||||
state: KerberosState::Final,
|
||||
config: KerberosConfig {
|
||||
kdc_url: None,
|
||||
client_computer_name: "hostname".into(),
|
||||
},
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams {
|
||||
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
|
||||
session_key: Some(session_key.to_vec().into()),
|
||||
sub_session_key: Some(sub_session_key.to_vec().into()),
|
||||
sspi_encrypt_key_usage: ACCEPTOR_SEAL,
|
||||
sspi_decrypt_key_usage: INITIATOR_SEAL,
|
||||
ec: 16,
|
||||
},
|
||||
seq_number: 681238048,
|
||||
realm: None,
|
||||
kdc_url: None,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: Some(Box::new(test_data::fake_server_properties())),
|
||||
};
|
||||
|
||||
// RPC header
|
||||
let header = [
|
||||
5, 0, 0, 3, 16, 0, 0, 0, 60, 1, 76, 0, 1, 0, 0, 0, 208, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// RPC security trailer header
|
||||
let trailer = [16, 6, 8, 0, 0, 0, 0, 0];
|
||||
// Encrypted data in RPC Request
|
||||
let enc_data = [
|
||||
41, 85, 192, 239, 104, 188, 180, 100, 229, 73, 83, 199, 77, 83, 79, 17, 163, 206, 241, 29, 90, 28, 89, 203, 83,
|
||||
176, 160, 252, 197, 221, 76, 113, 185, 141, 16, 200, 149, 55, 32, 96, 29, 49, 57, 124, 181, 147, 110, 198, 125,
|
||||
116, 150, 47, 35, 224, 117, 25, 10, 229, 201, 222, 153, 101, 131, 93, 204, 32, 9, 145, 186, 45, 224, 160, 131,
|
||||
23, 236, 111, 88, 48, 54, 4, 118, 114, 129, 119, 130, 164, 178, 4, 110, 74, 37, 1, 215, 177, 16, 204, 238, 83,
|
||||
255, 40, 240, 32, 209, 213, 90, 19, 126, 58, 34, 33, 72, 15, 206, 96, 67, 15, 169, 248, 176, 9, 173, 196, 159,
|
||||
239, 250, 120, 206, 52, 53, 229, 230, 66, 64, 109, 100, 21, 77, 193, 3, 40, 183, 209, 177, 152, 165, 171, 108,
|
||||
151, 112, 134, 53, 165, 128, 145, 147, 167, 5, 72, 35, 101, 42, 183, 67, 101, 48, 255, 84, 208, 112, 199, 154,
|
||||
62, 185, 87, 204, 228, 45, 30, 184, 47, 129, 145, 245, 168, 118, 174, 48, 98, 174, 167, 208, 0, 113, 246, 219,
|
||||
29, 192, 171, 97, 117, 115, 120, 115, 45, 44, 113, 62, 39,
|
||||
];
|
||||
// Unencrypted data in RPC Request
|
||||
let plaintext = [
|
||||
108, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 128, 84, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 20, 0,
|
||||
0, 0, 2, 0, 64, 0, 2, 0, 0, 0, 0, 0, 36, 0, 3, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 223, 243, 137, 88,
|
||||
86, 131, 83, 53, 105, 218, 109, 33, 80, 4, 0, 0, 0, 0, 20, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 138, 227, 19, 113, 2, 244, 54, 113, 2, 64, 40, 0,
|
||||
96, 89, 120, 185, 79, 82, 223, 17, 139, 109, 131, 220, 222, 215, 32, 133, 1, 0, 0, 0, 51, 5, 113, 113, 186,
|
||||
190, 55, 73, 131, 25, 181, 219, 239, 156, 204, 54, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// RPC Request security trailer data. Basically, it's a GSS API Wrap token
|
||||
let security_trailer_data = [
|
||||
5, 4, 6, 255, 0, 16, 0, 28, 0, 0, 0, 0, 40, 154, 222, 33, 170, 177, 218, 93, 176, 5, 210, 44, 38, 242, 179,
|
||||
168, 249, 202, 242, 199, 63, 162, 33, 40, 106, 186, 187, 28, 11, 229, 207, 219, 66, 86, 243, 16, 158, 100, 133,
|
||||
159, 87, 153, 196, 14, 251, 169, 164, 12, 18, 85, 182, 56, 72, 30, 137, 238, 50, 122, 73, 95, 109, 194, 60,
|
||||
120,
|
||||
];
|
||||
|
||||
let mut header_data = header.to_vec();
|
||||
let mut encrypted_data = enc_data.to_vec();
|
||||
let mut trailer_data = trailer.to_vec();
|
||||
let mut token_data = security_trailer_data.to_vec();
|
||||
let mut message = vec![
|
||||
SecurityBufferRef::data_buf(&mut header_data).with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::data_buf(&mut encrypted_data),
|
||||
SecurityBufferRef::data_buf(&mut trailer_data)
|
||||
.with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::token_buf(&mut token_data),
|
||||
];
|
||||
|
||||
kerberos_server.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(header[..], message[0].data()[..]);
|
||||
assert_eq!(plaintext[..], message[1].data()[..]);
|
||||
assert_eq!(trailer[..], message[2].data()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_request_encryption() {
|
||||
let mut kerberos_server = test_data::fake_server();
|
||||
let mut kerberos_client = test_data::fake_client();
|
||||
|
||||
// RPC header
|
||||
let header = [
|
||||
5, 0, 0, 3, 16, 0, 0, 0, 60, 1, 76, 0, 1, 0, 0, 0, 208, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// Unencrypted data in RPC Request
|
||||
let plaintext = [
|
||||
108, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 128, 84, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 20, 0,
|
||||
0, 0, 2, 0, 64, 0, 2, 0, 0, 0, 0, 0, 36, 0, 3, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 223, 243, 137, 88,
|
||||
86, 131, 83, 53, 105, 218, 109, 33, 80, 4, 0, 0, 0, 0, 20, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 138, 227, 19, 113, 2, 244, 54, 113, 2, 64, 40, 0,
|
||||
96, 89, 120, 185, 79, 82, 223, 17, 139, 109, 131, 220, 222, 215, 32, 133, 1, 0, 0, 0, 51, 5, 113, 113, 186,
|
||||
190, 55, 73, 131, 25, 181, 219, 239, 156, 204, 54, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// RPC security trailer header
|
||||
let trailer = [16, 6, 8, 0, 0, 0, 0, 0];
|
||||
|
||||
let mut header_data = header.to_vec();
|
||||
let mut data = plaintext.to_vec();
|
||||
let mut trailer_data = trailer.to_vec();
|
||||
let mut token_data = vec![0; 76];
|
||||
let mut message = vec![
|
||||
SecurityBufferRef::data_buf(&mut header_data).with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::data_buf(&mut data),
|
||||
SecurityBufferRef::data_buf(&mut trailer_data)
|
||||
.with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
|
||||
SecurityBufferRef::token_buf(&mut token_data),
|
||||
];
|
||||
|
||||
kerberos_client
|
||||
.encrypt_message(EncryptionFlags::empty(), &mut message)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(header[..], message[0].data()[..]);
|
||||
assert_eq!(trailer[..], message[2].data()[..]);
|
||||
|
||||
kerberos_server.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(header[..], message[0].data()[..]);
|
||||
assert_eq!(message[1].data(), plaintext);
|
||||
assert_eq!(trailer[..], message[2].data()[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_only_wrap_decryption() {
|
||||
// A standards-compliant SASL/GSSAPI peer (RFC 4752 / RFC 4121 §4.2.4) may
|
||||
// send an integrity-only (conf=false) Wrap token: the payload is carried in
|
||||
// the clear followed by the get_mic checksum, right-rotated by RRC. This
|
||||
// exercises the unsealed WRAP verification path (RRC undo, EC/RRC header
|
||||
// zeroing for the checksum input, and checksum validation) by feeding a
|
||||
// known-good token built the way a compliant sender would and asserting the
|
||||
// cleartext is recovered.
|
||||
use picky_krb::crypto::aes::{AesSize, checksum_sha_aes};
|
||||
use picky_krb::gss_api::WrapToken;
|
||||
|
||||
// Arbitrary AES-256 session key.
|
||||
let session_key = [
|
||||
0x9e, 0x8f, 0x7d, 0x6c, 0x5b, 0x4a, 0x39, 0x28, 0x17, 0x06, 0xf5, 0xe4, 0xd3, 0xc2, 0xb1, 0xa0, 0x10, 0x21,
|
||||
0x32, 0x43, 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9, 0xba, 0xcb, 0xdc, 0xed, 0xfe, 0x0f,
|
||||
];
|
||||
|
||||
let plaintext: &[u8] = b"GSSAPI integrity-only payload";
|
||||
|
||||
// The acceptor sends the token; a client (initiator) decrypts it with the
|
||||
// ACCEPTOR_SEAL key usage. Wrap tokens — sealed or not — use the SEAL usage.
|
||||
let key_usage = ACCEPTOR_SEAL;
|
||||
let aes_size = AesSize::Aes256;
|
||||
|
||||
// The checksum is computed over `plaintext | header`, with the EC and RRC
|
||||
// fields of the 16-octet header zeroed.
|
||||
let seq_num = 42;
|
||||
let mut wrap_token = WrapToken::with_seq_number(seq_num);
|
||||
// SentByAcceptor (0x01) set, Sealed (0x02) clear -> integrity-only.
|
||||
wrap_token.flags = 0x01;
|
||||
wrap_token.ec = 0;
|
||||
wrap_token.rrc = 0;
|
||||
|
||||
let mut to_sign = plaintext.to_vec();
|
||||
to_sign.extend_from_slice(&wrap_token.header());
|
||||
let checksum = checksum_sha_aes(&session_key, key_usage, &to_sign, &aes_size).unwrap();
|
||||
|
||||
// For unsealed tokens, EC encodes the trailing checksum length (RFC 4121
|
||||
// §4.2.3). The sender right-rotates `plaintext | checksum` by RRC.
|
||||
let rrc = u16::try_from(checksum.len()).unwrap();
|
||||
let mut payload = plaintext.to_vec();
|
||||
payload.extend_from_slice(&checksum);
|
||||
payload.rotate_right(usize::from(rrc));
|
||||
|
||||
wrap_token.ec = rrc;
|
||||
wrap_token.rrc = rrc;
|
||||
wrap_token.checksum = payload;
|
||||
|
||||
let mut token_bytes = Vec::new();
|
||||
wrap_token.encode(&mut token_bytes).unwrap();
|
||||
|
||||
let mut kerberos_client = Kerberos {
|
||||
state: KerberosState::Final,
|
||||
config: KerberosConfig {
|
||||
kdc_url: None,
|
||||
client_computer_name: "hostname".into(),
|
||||
},
|
||||
auth_identity: None,
|
||||
encryption_params: EncryptionParams {
|
||||
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
|
||||
session_key: None,
|
||||
sub_session_key: Some(session_key.to_vec().into()),
|
||||
sspi_encrypt_key_usage: INITIATOR_SEAL,
|
||||
sspi_decrypt_key_usage: ACCEPTOR_SEAL,
|
||||
ec: 0,
|
||||
},
|
||||
seq_number: 0,
|
||||
realm: None,
|
||||
kdc_url: None,
|
||||
channel_bindings: None,
|
||||
#[cfg(feature = "scard")]
|
||||
dh_parameters: None,
|
||||
krb5_user_to_user: false,
|
||||
server: None,
|
||||
};
|
||||
|
||||
let mut buffer = token_bytes;
|
||||
let mut message = [
|
||||
SecurityBufferRef::stream_buf(&mut buffer),
|
||||
SecurityBufferRef::data_buf(&mut []),
|
||||
];
|
||||
|
||||
kerberos_client.decrypt_message(&mut message).unwrap();
|
||||
|
||||
assert_eq!(message[1].data(), plaintext);
|
||||
}
|
||||
Reference in New Issue
Block a user