ci / rust (push) Canceled after 0s
ci / native-client (push) Canceled after 0s
ci / package-windows-client (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
1186 lines
42 KiB
Rust
1186 lines
42 KiB
Rust
use crate::{ChallengeMacVerification, HelperProcessIdentity, PipePeerVerifier, SessionMaterial};
|
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
|
use hmac::{Hmac, KeyInit as _, Mac as _};
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Digest as _, Sha256};
|
|
#[cfg(windows)]
|
|
use std::time::Instant;
|
|
use std::{
|
|
fmt,
|
|
fs::File,
|
|
io::{self, Read, Write},
|
|
path::Path,
|
|
time::Duration,
|
|
};
|
|
use zeroize::{Zeroize as _, Zeroizing};
|
|
|
|
const PIPE_PROTOCOL_VERSION: u8 = 1;
|
|
const PIPE_KEY_BYTES: usize = 32;
|
|
const PIPE_NONCE_BYTES: usize = 32;
|
|
const MAX_PIPE_FRAME_BYTES: usize = 64 * 1024;
|
|
const CONNECT_POLL_INTERVAL: Duration = Duration::from_millis(10);
|
|
const CLIENT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
const AUTH_DOMAIN: &[u8] = b"RemoteDesk secure pipe auth v1\0";
|
|
const SESSION_DOMAIN: &[u8] = b"RemoteDesk secure pipe session v1\0";
|
|
const SESSION_ACK_DOMAIN: &[u8] = b"RemoteDesk secure pipe session ack v1\0";
|
|
|
|
pub const PIPE_BOOTSTRAP_KEY_ENV: &str = "REMOTEDESK_PIPE_BOOTSTRAP_KEY";
|
|
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
|
|
#[derive(Clone, Deserialize, PartialEq, Eq, Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct RdpViewerLaunchConfig {
|
|
pub schema_version: u8,
|
|
pub target: String,
|
|
pub username: String,
|
|
pub width: u16,
|
|
pub height: u16,
|
|
pub use_multimon: bool,
|
|
#[serde(default)]
|
|
pub monitor_indices: Vec<u8>,
|
|
pub fullscreen: bool,
|
|
pub redirect_clipboard: bool,
|
|
pub session_id: String,
|
|
pub certificate_sha256: String,
|
|
pub credential_ref: Option<String>,
|
|
}
|
|
|
|
pub struct SecurePipeBootstrap {
|
|
pipe_name: String,
|
|
encoded_key: Zeroizing<String>,
|
|
}
|
|
|
|
impl fmt::Debug for SecurePipeBootstrap {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("SecurePipeBootstrap")
|
|
.field("pipe_name", &self.pipe_name)
|
|
.field("encoded_key", &"[REDACTED]")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl SecurePipeBootstrap {
|
|
#[must_use]
|
|
pub fn pipe_name(&self) -> &str {
|
|
&self.pipe_name
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn encoded_key(&self) -> &str {
|
|
&self.encoded_key
|
|
}
|
|
}
|
|
|
|
pub struct SecurePipeServer {
|
|
#[cfg(windows)]
|
|
pipe: File,
|
|
key: Zeroizing<Vec<u8>>,
|
|
}
|
|
|
|
pub struct SecurePipeClientSession {
|
|
#[cfg(windows)]
|
|
pipe: File,
|
|
#[cfg(windows)]
|
|
deadline: Instant,
|
|
key: Zeroizing<Vec<u8>>,
|
|
nonce: Vec<u8>,
|
|
payload: Vec<u8>,
|
|
}
|
|
|
|
impl fmt::Debug for SecurePipeClientSession {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("SecurePipeClientSession")
|
|
.field("key", &"[REDACTED]")
|
|
.field("payload", &"[REDACTED]")
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl SecurePipeClientSession {
|
|
#[must_use]
|
|
pub fn payload(&self) -> &[u8] {
|
|
&self.payload
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn acknowledge(mut self) -> io::Result<()> {
|
|
ensure_handshake_deadline(self.deadline)?;
|
|
let digest = payload_digest(&self.payload);
|
|
let mac = session_ack_mac(&self.key, &self.nonce, &digest);
|
|
write_json_frame_until(
|
|
&mut self.pipe,
|
|
&SessionAckMessage {
|
|
protocol_version: PIPE_PROTOCOL_VERSION,
|
|
payload_sha256: URL_SAFE_NO_PAD.encode(digest),
|
|
mac: URL_SAFE_NO_PAD.encode(mac),
|
|
},
|
|
self.deadline,
|
|
)?;
|
|
self.pipe.flush()
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn acknowledge(self) -> io::Result<()> {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"secure Named Pipes require Windows",
|
|
))
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for SecurePipeServer {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("SecurePipeServer")
|
|
.field("key", &"[REDACTED]")
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct ChallengeMessage {
|
|
protocol_version: u8,
|
|
nonce: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct AuthenticationMessage {
|
|
protocol_version: u8,
|
|
pid: u32,
|
|
creation_token: u64,
|
|
build_hash: String,
|
|
mac: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct SessionMessage {
|
|
protocol_version: u8,
|
|
payload: String,
|
|
mac: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct SessionAckMessage {
|
|
protocol_version: u8,
|
|
payload_sha256: String,
|
|
mac: String,
|
|
}
|
|
|
|
impl SecurePipeServer {
|
|
#[cfg(windows)]
|
|
pub fn create(session_id: &str) -> io::Result<(Self, SecurePipeBootstrap)> {
|
|
if session_id.is_empty()
|
|
|| session_id.len() > 64
|
|
|| !session_id
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
|
{
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"invalid secure pipe session identifier",
|
|
));
|
|
}
|
|
let mut key = Zeroizing::new(vec![0_u8; PIPE_KEY_BYTES]);
|
|
getrandom::fill(&mut key).map_err(io::Error::other)?;
|
|
let mut suffix = [0_u8; 16];
|
|
getrandom::fill(&mut suffix).map_err(io::Error::other)?;
|
|
let pipe_name = format!(
|
|
r"\\.\pipe\RemoteDesk\rdp-{session_id}-{}",
|
|
URL_SAFE_NO_PAD.encode(suffix)
|
|
);
|
|
let pipe = windows_platform::create_current_user_pipe(&pipe_name)?;
|
|
let bootstrap = SecurePipeBootstrap {
|
|
pipe_name,
|
|
encoded_key: Zeroizing::new(URL_SAFE_NO_PAD.encode(&*key)),
|
|
};
|
|
Ok((Self { pipe, key }, bootstrap))
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn create(_session_id: &str) -> io::Result<(Self, SecurePipeBootstrap)> {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"secure Named Pipes require Windows",
|
|
))
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn authenticate_and_send(
|
|
mut self,
|
|
expected: HelperProcessIdentity,
|
|
payload: &[u8],
|
|
timeout: Duration,
|
|
) -> io::Result<()> {
|
|
if payload.len() > MAX_PIPE_FRAME_BYTES / 2 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"secure pipe session payload is too large",
|
|
));
|
|
}
|
|
let deadline = Instant::now()
|
|
.checked_add(timeout)
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid pipe timeout"))?;
|
|
windows_platform::wait_for_client(&self.pipe, timeout)?;
|
|
windows_platform::set_nonblocking(&self.pipe)?;
|
|
let pipe_pid = windows_platform::pipe_client_pid(&self.pipe)?;
|
|
if pipe_pid != expected.pid {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"secure pipe client PID does not match the spawned helper",
|
|
));
|
|
}
|
|
|
|
let mut nonce = [0_u8; PIPE_NONCE_BYTES];
|
|
getrandom::fill(&mut nonce).map_err(io::Error::other)?;
|
|
write_json_frame_until(
|
|
&mut self.pipe,
|
|
&ChallengeMessage {
|
|
protocol_version: PIPE_PROTOCOL_VERSION,
|
|
nonce: URL_SAFE_NO_PAD.encode(nonce),
|
|
},
|
|
deadline,
|
|
)?;
|
|
let auth: AuthenticationMessage = read_json_frame_until(&mut self.pipe, deadline)?;
|
|
if auth.protocol_version != PIPE_PROTOCOL_VERSION {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe protocol version mismatch",
|
|
));
|
|
}
|
|
let claimed = HelperProcessIdentity::new(auth.pid, auth.creation_token, auth.build_hash);
|
|
let mac = URL_SAFE_NO_PAD
|
|
.decode(auth.mac)
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe auth MAC"))?;
|
|
let mac_verified = verify_identity_mac(&self.key, AUTH_DOMAIN, &nonce, &claimed, &mac);
|
|
let mut verifier = PipePeerVerifier::new(expected);
|
|
verifier
|
|
.authenticate(claimed, mac_verified)
|
|
.map_err(|error| io::Error::new(io::ErrorKind::PermissionDenied, error))?;
|
|
let material = SessionMaterial::new(payload.to_vec());
|
|
let authenticated_payload = verifier
|
|
.session_material(&material)
|
|
.map_err(|error| io::Error::new(io::ErrorKind::PermissionDenied, error))?;
|
|
let session_mac = session_mac(&self.key, &nonce, authenticated_payload);
|
|
write_json_frame_until(
|
|
&mut self.pipe,
|
|
&SessionMessage {
|
|
protocol_version: PIPE_PROTOCOL_VERSION,
|
|
payload: URL_SAFE_NO_PAD.encode(authenticated_payload),
|
|
mac: URL_SAFE_NO_PAD.encode(session_mac),
|
|
},
|
|
deadline,
|
|
)?;
|
|
self.pipe.flush()?;
|
|
let acknowledgement: SessionAckMessage = read_json_frame_until(&mut self.pipe, deadline)?;
|
|
if acknowledgement.protocol_version != PIPE_PROTOCOL_VERSION {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe acknowledgement version mismatch",
|
|
));
|
|
}
|
|
let acknowledged_digest = URL_SAFE_NO_PAD
|
|
.decode(acknowledgement.payload_sha256)
|
|
.map_err(|_| {
|
|
io::Error::new(io::ErrorKind::InvalidData, "invalid pipe payload digest")
|
|
})?;
|
|
let expected_digest = payload_digest(authenticated_payload);
|
|
if acknowledged_digest.as_slice() != expected_digest.as_slice() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"secure pipe payload acknowledgement does not match",
|
|
));
|
|
}
|
|
let acknowledgement_mac = URL_SAFE_NO_PAD
|
|
.decode(acknowledgement.mac)
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe ACK MAC"))?;
|
|
if !verify_session_ack_mac(&self.key, &nonce, &expected_digest, &acknowledgement_mac) {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"secure pipe acknowledgement MAC was rejected",
|
|
));
|
|
}
|
|
ensure_handshake_deadline(deadline)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn receive_secure_pipe_session(pipe_name: &str) -> io::Result<SecurePipeClientSession> {
|
|
if !pipe_name.starts_with(r"\\.\pipe\RemoteDesk\rdp-") || pipe_name.len() > 256 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"invalid RemoteDesk secure pipe name",
|
|
));
|
|
}
|
|
let encoded_key = std::env::var(PIPE_BOOTSTRAP_KEY_ENV)
|
|
.map_err(|_| io::Error::new(io::ErrorKind::PermissionDenied, "pipe key is missing"))?;
|
|
// The helper reads the one-time bootstrap before starting any worker thread.
|
|
unsafe { std::env::remove_var(PIPE_BOOTSTRAP_KEY_ENV) };
|
|
let mut encoded_key = Zeroizing::new(encoded_key);
|
|
let key = Zeroizing::new(
|
|
URL_SAFE_NO_PAD
|
|
.decode(encoded_key.as_bytes())
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pipe key is invalid"))?,
|
|
);
|
|
encoded_key.zeroize();
|
|
if key.len() != PIPE_KEY_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"pipe key has an invalid length",
|
|
));
|
|
}
|
|
receive_secure_pipe_session_with_key(pipe_name, key)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn receive_secure_pipe_session_with_key(
|
|
pipe_name: &str,
|
|
key: Zeroizing<Vec<u8>>,
|
|
) -> io::Result<SecurePipeClientSession> {
|
|
if !pipe_name.starts_with(r"\\.\pipe\RemoteDesk\rdp-") || pipe_name.len() > 256 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"invalid RemoteDesk secure pipe name",
|
|
));
|
|
}
|
|
if key.len() != PIPE_KEY_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"pipe key has an invalid length",
|
|
));
|
|
}
|
|
let deadline = Instant::now()
|
|
.checked_add(CLIENT_CONNECT_TIMEOUT)
|
|
.expect("fixed secure pipe timeout fits Instant");
|
|
let mut pipe = windows_platform::connect_client(pipe_name, CLIENT_CONNECT_TIMEOUT)?;
|
|
let challenge: ChallengeMessage = read_json_frame_until(&mut pipe, deadline)?;
|
|
if challenge.protocol_version != PIPE_PROTOCOL_VERSION {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe protocol version mismatch",
|
|
));
|
|
}
|
|
let nonce = URL_SAFE_NO_PAD
|
|
.decode(challenge.nonce)
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe nonce"))?;
|
|
if nonce.len() != PIPE_NONCE_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"pipe nonce has an invalid length",
|
|
));
|
|
}
|
|
let identity = current_process_identity()?;
|
|
let auth_mac = identity_mac(&key, AUTH_DOMAIN, &nonce, &identity);
|
|
write_json_frame_until(
|
|
&mut pipe,
|
|
&AuthenticationMessage {
|
|
protocol_version: PIPE_PROTOCOL_VERSION,
|
|
pid: identity.pid,
|
|
creation_token: identity.creation_token,
|
|
build_hash: identity.build_hash.clone(),
|
|
mac: URL_SAFE_NO_PAD.encode(auth_mac),
|
|
},
|
|
deadline,
|
|
)?;
|
|
let session: SessionMessage = read_json_frame_until(&mut pipe, deadline)?;
|
|
if session.protocol_version != PIPE_PROTOCOL_VERSION {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe protocol version mismatch",
|
|
));
|
|
}
|
|
let payload = URL_SAFE_NO_PAD
|
|
.decode(session.payload)
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe payload"))?;
|
|
let mac = URL_SAFE_NO_PAD
|
|
.decode(session.mac)
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe session MAC"))?;
|
|
if !verify_session_mac(&key, &nonce, &payload, &mac) {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"secure pipe session MAC was rejected",
|
|
));
|
|
}
|
|
Ok(SecurePipeClientSession {
|
|
pipe,
|
|
deadline,
|
|
key,
|
|
nonce,
|
|
payload,
|
|
})
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn receive_secure_pipe_session(_pipe_name: &str) -> io::Result<SecurePipeClientSession> {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"secure Named Pipes require Windows",
|
|
))
|
|
}
|
|
|
|
pub fn receive_secure_pipe_payload(pipe_name: &str) -> io::Result<Vec<u8>> {
|
|
let session = receive_secure_pipe_session(pipe_name)?;
|
|
let payload = session.payload().to_vec();
|
|
session.acknowledge()?;
|
|
Ok(payload)
|
|
}
|
|
|
|
pub fn current_process_identity() -> io::Result<HelperProcessIdentity> {
|
|
#[cfg(windows)]
|
|
{
|
|
let executable = std::env::current_exe()?;
|
|
process_identity(std::process::id(), &executable)
|
|
}
|
|
#[cfg(not(windows))]
|
|
{
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"Windows process identity is unavailable",
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn process_identity(pid: u32, expected_executable: &Path) -> io::Result<HelperProcessIdentity> {
|
|
#[cfg(windows)]
|
|
{
|
|
windows_platform::process_identity(pid, expected_executable)
|
|
}
|
|
#[cfg(not(windows))]
|
|
{
|
|
let _ = (pid, expected_executable);
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"Windows process identity is unavailable",
|
|
))
|
|
}
|
|
}
|
|
|
|
fn identity_mac(
|
|
key: &[u8],
|
|
domain: &[u8],
|
|
nonce: &[u8],
|
|
identity: &HelperProcessIdentity,
|
|
) -> Vec<u8> {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
|
|
update_identity_mac(&mut mac, domain, nonce, identity);
|
|
mac.finalize().into_bytes().to_vec()
|
|
}
|
|
|
|
fn update_identity_mac(
|
|
mac: &mut HmacSha256,
|
|
domain: &[u8],
|
|
nonce: &[u8],
|
|
identity: &HelperProcessIdentity,
|
|
) {
|
|
mac.update(domain);
|
|
mac.update(nonce);
|
|
mac.update(&identity.pid.to_le_bytes());
|
|
mac.update(&identity.creation_token.to_le_bytes());
|
|
mac.update(identity.build_hash.as_bytes());
|
|
}
|
|
|
|
fn verify_identity_mac(
|
|
key: &[u8],
|
|
domain: &[u8],
|
|
nonce: &[u8],
|
|
identity: &HelperProcessIdentity,
|
|
candidate: &[u8],
|
|
) -> ChallengeMacVerification {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
|
|
update_identity_mac(&mut mac, domain, nonce, identity);
|
|
if mac.verify_slice(candidate).is_ok() {
|
|
ChallengeMacVerification::Verified
|
|
} else {
|
|
ChallengeMacVerification::Rejected
|
|
}
|
|
}
|
|
|
|
fn session_mac(key: &[u8], nonce: &[u8], payload: &[u8]) -> Vec<u8> {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
|
|
update_session_mac(&mut mac, nonce, payload);
|
|
mac.finalize().into_bytes().to_vec()
|
|
}
|
|
|
|
fn verify_session_mac(key: &[u8], nonce: &[u8], payload: &[u8], candidate: &[u8]) -> bool {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
|
|
update_session_mac(&mut mac, nonce, payload);
|
|
mac.verify_slice(candidate).is_ok()
|
|
}
|
|
|
|
fn update_session_mac(mac: &mut HmacSha256, nonce: &[u8], payload: &[u8]) {
|
|
mac.update(SESSION_DOMAIN);
|
|
mac.update(nonce);
|
|
mac.update(&(payload.len() as u64).to_le_bytes());
|
|
mac.update(payload);
|
|
}
|
|
|
|
fn payload_digest(payload: &[u8]) -> [u8; 32] {
|
|
Sha256::digest(payload).into()
|
|
}
|
|
|
|
fn session_ack_mac(key: &[u8], nonce: &[u8], digest: &[u8; 32]) -> Vec<u8> {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
|
|
update_session_ack_mac(&mut mac, nonce, digest);
|
|
mac.finalize().into_bytes().to_vec()
|
|
}
|
|
|
|
fn verify_session_ack_mac(key: &[u8], nonce: &[u8], digest: &[u8; 32], candidate: &[u8]) -> bool {
|
|
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts arbitrary key lengths");
|
|
update_session_ack_mac(&mut mac, nonce, digest);
|
|
mac.verify_slice(candidate).is_ok()
|
|
}
|
|
|
|
fn update_session_ack_mac(mac: &mut HmacSha256, nonce: &[u8], digest: &[u8; 32]) {
|
|
mac.update(SESSION_ACK_DOMAIN);
|
|
mac.update(nonce);
|
|
mac.update(digest);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn write_json_frame<T: Serialize>(writer: &mut impl Write, value: &T) -> io::Result<()> {
|
|
let encoded = serde_json::to_vec(value).map_err(io::Error::other)?;
|
|
if encoded.is_empty() || encoded.len() > MAX_PIPE_FRAME_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe frame size is invalid",
|
|
));
|
|
}
|
|
let length = u32::try_from(encoded.len())
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pipe frame is too large"))?;
|
|
writer.write_all(&length.to_le_bytes())?;
|
|
writer.write_all(&encoded)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn write_json_frame_until<T: Serialize>(
|
|
writer: &mut impl Write,
|
|
value: &T,
|
|
deadline: Instant,
|
|
) -> io::Result<()> {
|
|
let encoded = serde_json::to_vec(value).map_err(io::Error::other)?;
|
|
if encoded.is_empty() || encoded.len() > MAX_PIPE_FRAME_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe frame size is invalid",
|
|
));
|
|
}
|
|
let length = u32::try_from(encoded.len())
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pipe frame is too large"))?;
|
|
let mut frame = Vec::with_capacity(4 + encoded.len());
|
|
frame.extend_from_slice(&length.to_le_bytes());
|
|
frame.extend_from_slice(&encoded);
|
|
let mut offset = 0;
|
|
while offset < frame.len() {
|
|
ensure_handshake_deadline(deadline)?;
|
|
match writer.write(&frame[offset..]) {
|
|
Ok(0) => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::WriteZero,
|
|
"secure pipe write made no progress",
|
|
));
|
|
}
|
|
Ok(count) => offset += count,
|
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
|
Err(error) if windows_platform::read_is_pending(&error) => {
|
|
std::thread::sleep(CONNECT_POLL_INTERVAL);
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn read_json_frame<T: for<'de> Deserialize<'de>>(reader: &mut impl Read) -> io::Result<T> {
|
|
let mut length = [0_u8; 4];
|
|
reader.read_exact(&mut length)?;
|
|
let length = usize::try_from(u32::from_le_bytes(length))
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe frame length"))?;
|
|
if length == 0 || length > MAX_PIPE_FRAME_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe frame size is invalid",
|
|
));
|
|
}
|
|
let mut encoded = vec![0_u8; length];
|
|
reader.read_exact(&mut encoded)?;
|
|
serde_json::from_slice(&encoded).map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe frame is not valid protocol JSON",
|
|
)
|
|
})
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn read_json_frame_until<T: for<'de> Deserialize<'de>>(
|
|
reader: &mut impl Read,
|
|
deadline: Instant,
|
|
) -> io::Result<T> {
|
|
let mut length = [0_u8; 4];
|
|
read_exact_until(reader, &mut length, deadline)?;
|
|
let length = usize::try_from(u32::from_le_bytes(length))
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid pipe frame length"))?;
|
|
if length == 0 || length > MAX_PIPE_FRAME_BYTES {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe frame size is invalid",
|
|
));
|
|
}
|
|
let mut encoded = vec![0_u8; length];
|
|
read_exact_until(reader, &mut encoded, deadline)?;
|
|
serde_json::from_slice(&encoded).map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"secure pipe frame is not valid protocol JSON",
|
|
)
|
|
})
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn read_exact_until(
|
|
reader: &mut impl Read,
|
|
buffer: &mut [u8],
|
|
deadline: Instant,
|
|
) -> io::Result<()> {
|
|
let mut offset = 0;
|
|
while offset < buffer.len() {
|
|
ensure_handshake_deadline(deadline)?;
|
|
match reader.read(&mut buffer[offset..]) {
|
|
// In PIPE_NOWAIT mode Windows can report a successful zero-byte
|
|
// read while the connected peer has not produced data yet.
|
|
Ok(0) => std::thread::sleep(CONNECT_POLL_INTERVAL),
|
|
Ok(count) => offset += count,
|
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
|
Err(error) if windows_platform::read_is_pending(&error) => {
|
|
std::thread::sleep(CONNECT_POLL_INTERVAL);
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn ensure_handshake_deadline(deadline: Instant) -> io::Result<()> {
|
|
if Instant::now() >= deadline {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::TimedOut,
|
|
"secure pipe handshake timed out",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
mod windows_platform {
|
|
use super::{CONNECT_POLL_INTERVAL, HelperProcessIdentity, Sha256};
|
|
use sha2::Digest as _;
|
|
use std::{
|
|
ffi::c_void,
|
|
fs::{self, File, OpenOptions},
|
|
io, mem,
|
|
os::windows::io::{AsRawHandle as _, FromRawHandle as _, RawHandle},
|
|
path::Path,
|
|
ptr, thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
use windows_sys::Win32::{
|
|
Foundation::{
|
|
ERROR_INSUFFICIENT_BUFFER, ERROR_NO_DATA, ERROR_PIPE_CONNECTED, ERROR_PIPE_LISTENING,
|
|
FILETIME, HANDLE, INVALID_HANDLE_VALUE, LocalFree,
|
|
},
|
|
Security::{
|
|
Authorization::{
|
|
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
|
|
SDDL_REVISION_1,
|
|
},
|
|
GetTokenInformation, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY,
|
|
TOKEN_USER, TokenUser,
|
|
},
|
|
Storage::FileSystem::{FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_ACCESS_DUPLEX},
|
|
System::{
|
|
Pipes::{
|
|
ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, PIPE_NOWAIT,
|
|
PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE,
|
|
SetNamedPipeHandleState,
|
|
},
|
|
Threading::{
|
|
GetCurrentProcess, GetProcessTimes, OpenProcess, OpenProcessToken,
|
|
PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW,
|
|
},
|
|
},
|
|
};
|
|
|
|
pub fn create_current_user_pipe(pipe_name: &str) -> io::Result<File> {
|
|
let sid = current_user_sid()?;
|
|
let sddl = wide(&format!("D:P(A;;GA;;;{sid})"));
|
|
let mut descriptor: PSECURITY_DESCRIPTOR = ptr::null_mut();
|
|
// SAFETY: pointers reference writable output storage and a terminated SDDL string.
|
|
if unsafe {
|
|
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
sddl.as_ptr(),
|
|
SDDL_REVISION_1,
|
|
&mut descriptor,
|
|
ptr::null_mut(),
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
let attributes = SECURITY_ATTRIBUTES {
|
|
nLength: u32::try_from(mem::size_of::<SECURITY_ATTRIBUTES>())
|
|
.expect("SECURITY_ATTRIBUTES size fits u32"),
|
|
lpSecurityDescriptor: descriptor.cast(),
|
|
bInheritHandle: 0,
|
|
};
|
|
let pipe_name = wide(pipe_name);
|
|
// SAFETY: arguments are initialized and the security descriptor remains alive for the call.
|
|
let handle = unsafe {
|
|
CreateNamedPipeW(
|
|
pipe_name.as_ptr(),
|
|
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE,
|
|
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT | PIPE_REJECT_REMOTE_CLIENTS,
|
|
1,
|
|
64 * 1024,
|
|
64 * 1024,
|
|
5_000,
|
|
&attributes,
|
|
)
|
|
};
|
|
// SAFETY: the descriptor was allocated by LocalAlloc inside the conversion API.
|
|
unsafe { LocalFree(descriptor.cast()) };
|
|
if handle == INVALID_HANDLE_VALUE {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
// SAFETY: CreateNamedPipeW returned an owned kernel handle.
|
|
Ok(unsafe { File::from_raw_handle(handle as RawHandle) })
|
|
}
|
|
|
|
pub fn wait_for_client(pipe: &File, timeout: Duration) -> io::Result<()> {
|
|
let handle = pipe.as_raw_handle() as HANDLE;
|
|
let deadline = Instant::now() + timeout;
|
|
loop {
|
|
// SAFETY: handle is a live Named Pipe server handle and no OVERLAPPED is used.
|
|
if unsafe { ConnectNamedPipe(handle, ptr::null_mut()) } != 0 {
|
|
break;
|
|
}
|
|
let error = io::Error::last_os_error();
|
|
match error.raw_os_error().map(|value| value as u32) {
|
|
Some(ERROR_PIPE_CONNECTED) => break,
|
|
Some(ERROR_PIPE_LISTENING) if Instant::now() < deadline => {
|
|
thread::sleep(CONNECT_POLL_INTERVAL);
|
|
}
|
|
Some(ERROR_PIPE_LISTENING) => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::TimedOut,
|
|
"native helper did not connect to its secure pipe",
|
|
));
|
|
}
|
|
_ => return Err(error),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn pipe_client_pid(pipe: &File) -> io::Result<u32> {
|
|
let mut pid = 0_u32;
|
|
// SAFETY: handle is a connected Named Pipe server and pid is writable.
|
|
if unsafe { GetNamedPipeClientProcessId(pipe.as_raw_handle() as HANDLE, &mut pid) } == 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
Ok(pid)
|
|
}
|
|
|
|
pub fn connect_client(pipe_name: &str, timeout: Duration) -> io::Result<File> {
|
|
let deadline = Instant::now() + timeout;
|
|
loop {
|
|
match OpenOptions::new().read(true).write(true).open(pipe_name) {
|
|
Ok(pipe) => {
|
|
set_nonblocking(&pipe)?;
|
|
return Ok(pipe);
|
|
}
|
|
Err(error)
|
|
if matches!(
|
|
error.kind(),
|
|
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
|
|
) && Instant::now() < deadline =>
|
|
{
|
|
thread::sleep(CONNECT_POLL_INTERVAL);
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_nonblocking(pipe: &File) -> io::Result<()> {
|
|
let mode = PIPE_READMODE_BYTE | PIPE_NOWAIT;
|
|
// SAFETY: handle is a live Named Pipe client and mode points to initialized storage.
|
|
if unsafe {
|
|
SetNamedPipeHandleState(
|
|
pipe.as_raw_handle() as HANDLE,
|
|
&mode,
|
|
ptr::null(),
|
|
ptr::null(),
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn read_is_pending(error: &io::Error) -> bool {
|
|
error.kind() == io::ErrorKind::WouldBlock
|
|
|| error.raw_os_error().map(|value| value as u32) == Some(ERROR_NO_DATA)
|
|
}
|
|
|
|
pub fn process_identity(
|
|
pid: u32,
|
|
expected_executable: &Path,
|
|
) -> io::Result<HelperProcessIdentity> {
|
|
// SAFETY: OpenProcess returns a new owned handle or null.
|
|
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
|
|
if process.is_null() {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
// SAFETY: OpenProcess returned an owned process handle.
|
|
let process = unsafe { File::from_raw_handle(process as RawHandle) };
|
|
let handle = process.as_raw_handle() as HANDLE;
|
|
let mut creation = FILETIME::default();
|
|
let mut exit = FILETIME::default();
|
|
let mut kernel = FILETIME::default();
|
|
let mut user = FILETIME::default();
|
|
// SAFETY: all output pointers are valid for writes and handle is a process handle.
|
|
if unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) } == 0
|
|
{
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
let creation_token =
|
|
(u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime);
|
|
let mut image = vec![0_u16; 32_768];
|
|
let mut image_len = u32::try_from(image.len()).expect("image buffer length fits u32");
|
|
// SAFETY: image is writable for image_len UTF-16 code units.
|
|
if unsafe {
|
|
QueryFullProcessImageNameW(
|
|
handle,
|
|
PROCESS_NAME_WIN32,
|
|
image.as_mut_ptr(),
|
|
&mut image_len,
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
image.truncate(image_len as usize);
|
|
let actual_executable =
|
|
std::path::PathBuf::from(String::from_utf16(&image).map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
"helper image path is not valid UTF-16",
|
|
)
|
|
})?);
|
|
let expected = fs::canonicalize(expected_executable)?;
|
|
let actual = fs::canonicalize(&actual_executable)?;
|
|
if !actual
|
|
.to_string_lossy()
|
|
.eq_ignore_ascii_case(&expected.to_string_lossy())
|
|
{
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"secure pipe client image does not match the launched helper",
|
|
));
|
|
}
|
|
let build_hash = Sha256::digest(fs::read(actual)?).iter().fold(
|
|
String::with_capacity(64),
|
|
|mut output, byte| {
|
|
use std::fmt::Write as _;
|
|
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
|
|
output
|
|
},
|
|
);
|
|
Ok(HelperProcessIdentity::new(pid, creation_token, build_hash))
|
|
}
|
|
|
|
fn current_user_sid() -> io::Result<String> {
|
|
let mut token: HANDLE = ptr::null_mut();
|
|
// SAFETY: token points to writable handle storage; pseudo process handle is valid.
|
|
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
// SAFETY: OpenProcessToken returned an owned token handle.
|
|
let token = unsafe { File::from_raw_handle(token as RawHandle) };
|
|
let mut required = 0_u32;
|
|
// SAFETY: null buffer with zero length is the documented size query.
|
|
unsafe {
|
|
GetTokenInformation(
|
|
token.as_raw_handle() as HANDLE,
|
|
TokenUser,
|
|
ptr::null_mut(),
|
|
0,
|
|
&mut required,
|
|
)
|
|
};
|
|
let size_error = io::Error::last_os_error();
|
|
if size_error.raw_os_error().map(|value| value as u32) != Some(ERROR_INSUFFICIENT_BUFFER)
|
|
|| required == 0
|
|
{
|
|
return Err(size_error);
|
|
}
|
|
let word = mem::size_of::<usize>();
|
|
let words = (required as usize).div_ceil(word);
|
|
let mut buffer = vec![0_usize; words];
|
|
// SAFETY: buffer has at least required writable bytes and required is preserved.
|
|
if unsafe {
|
|
GetTokenInformation(
|
|
token.as_raw_handle() as HANDLE,
|
|
TokenUser,
|
|
buffer.as_mut_ptr().cast::<c_void>(),
|
|
required,
|
|
&mut required,
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
// SAFETY: TOKEN_USER is the documented structure returned for TokenUser.
|
|
let token_user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
|
|
let mut sid_string = ptr::null_mut();
|
|
// SAFETY: SID comes from a successful TokenUser query and output pointer is writable.
|
|
if unsafe { ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string) } == 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
let mut length = 0_usize;
|
|
// SAFETY: conversion returns a null-terminated LocalAlloc UTF-16 string.
|
|
unsafe {
|
|
while *sid_string.add(length) != 0 {
|
|
length += 1;
|
|
}
|
|
}
|
|
// SAFETY: the preceding scan established a valid initialized slice.
|
|
let sid = String::from_utf16(unsafe { std::slice::from_raw_parts(sid_string, length) })
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "current SID is invalid"));
|
|
// SAFETY: sid_string was allocated by ConvertSidToStringSidW.
|
|
unsafe { LocalFree(sid_string.cast()) };
|
|
sid
|
|
}
|
|
|
|
fn wide(value: &str) -> Vec<u16> {
|
|
value.encode_utf16().chain([0]).collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn frame_codec_is_bounded_and_strict() {
|
|
let message = ChallengeMessage {
|
|
protocol_version: 1,
|
|
nonce: "nonce".to_owned(),
|
|
};
|
|
let mut bytes = Vec::new();
|
|
write_json_frame(&mut bytes, &message).unwrap();
|
|
let decoded: ChallengeMessage = read_json_frame(&mut bytes.as_slice()).unwrap();
|
|
assert_eq!(decoded.protocol_version, 1);
|
|
assert_eq!(decoded.nonce, "nonce");
|
|
|
|
let oversized = u32::try_from(MAX_PIPE_FRAME_BYTES + 1)
|
|
.unwrap()
|
|
.to_le_bytes();
|
|
assert!(read_json_frame::<ChallengeMessage>(&mut oversized.as_slice()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn challenge_and_payload_macs_bind_all_fields() {
|
|
let key = [7_u8; PIPE_KEY_BYTES];
|
|
let nonce = [9_u8; PIPE_NONCE_BYTES];
|
|
let identity = HelperProcessIdentity::new(42, 100, "build-a");
|
|
let mac = identity_mac(&key, AUTH_DOMAIN, &nonce, &identity);
|
|
assert_eq!(
|
|
verify_identity_mac(&key, AUTH_DOMAIN, &nonce, &identity, &mac),
|
|
ChallengeMacVerification::Verified
|
|
);
|
|
assert_eq!(
|
|
verify_identity_mac(
|
|
&key,
|
|
AUTH_DOMAIN,
|
|
&nonce,
|
|
&HelperProcessIdentity::new(42, 101, "build-a"),
|
|
&mac,
|
|
),
|
|
ChallengeMacVerification::Rejected
|
|
);
|
|
assert_ne!(
|
|
session_mac(&key, &nonce, b"one"),
|
|
session_mac(&key, &nonce, b"two")
|
|
);
|
|
let session_signature = session_mac(&key, &nonce, b"payload");
|
|
assert!(verify_session_mac(
|
|
&key,
|
|
&nonce,
|
|
b"payload",
|
|
&session_signature
|
|
));
|
|
assert!(!verify_session_mac(
|
|
&key,
|
|
&nonce,
|
|
b"tampered",
|
|
&session_signature
|
|
));
|
|
|
|
let digest = payload_digest(b"payload");
|
|
let acknowledgement = session_ack_mac(&key, &nonce, &digest);
|
|
assert!(verify_session_ack_mac(
|
|
&key,
|
|
&nonce,
|
|
&digest,
|
|
&acknowledgement
|
|
));
|
|
assert!(!verify_session_ack_mac(
|
|
&key,
|
|
&nonce,
|
|
&payload_digest(b"other payload"),
|
|
&acknowledgement
|
|
));
|
|
let mut tampered_acknowledgement = acknowledgement;
|
|
tampered_acknowledgement[0] ^= 1;
|
|
assert!(!verify_session_ack_mac(
|
|
&key,
|
|
&nonce,
|
|
&digest,
|
|
&tampered_acknowledgement
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn launch_config_rejects_unknown_fields() {
|
|
let value = serde_json::json!({
|
|
"schema_version": 2,
|
|
"target": "host",
|
|
"username": "user",
|
|
"width": 1920,
|
|
"height": 1080,
|
|
"use_multimon": false,
|
|
"fullscreen": false,
|
|
"redirect_clipboard": true,
|
|
"session_id": "1-2-3",
|
|
"certificate_sha256": "ab".repeat(32),
|
|
"credential_ref": null,
|
|
"password": "must-not-be-accepted"
|
|
});
|
|
assert!(serde_json::from_value::<RdpViewerLaunchConfig>(value).is_err());
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn real_windows_pipe_authenticates_process_and_payload() {
|
|
let session_id = format!("test-{}-1", std::process::id());
|
|
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
|
|
let key = URL_SAFE_NO_PAD
|
|
.decode(bootstrap.encoded_key().as_bytes())
|
|
.unwrap();
|
|
let pipe_name = bootstrap.pipe_name().to_owned();
|
|
let expected = current_process_identity().unwrap();
|
|
let client = std::thread::spawn(move || {
|
|
let session =
|
|
receive_secure_pipe_session_with_key(&pipe_name, Zeroizing::new(key)).unwrap();
|
|
let payload = session.payload().to_vec();
|
|
session.acknowledge().unwrap();
|
|
payload
|
|
});
|
|
|
|
server
|
|
.authenticate_and_send(expected, b"authenticated-session", Duration::from_secs(2))
|
|
.unwrap();
|
|
assert_eq!(client.join().unwrap(), b"authenticated-session");
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn real_windows_pipe_rejects_mismatched_payload_acknowledgement() {
|
|
let session_id = format!("test-{}-2", std::process::id());
|
|
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
|
|
let key = URL_SAFE_NO_PAD
|
|
.decode(bootstrap.encoded_key().as_bytes())
|
|
.unwrap();
|
|
let pipe_name = bootstrap.pipe_name().to_owned();
|
|
let expected = current_process_identity().unwrap();
|
|
let client = std::thread::spawn(move || {
|
|
let mut session =
|
|
receive_secure_pipe_session_with_key(&pipe_name, Zeroizing::new(key)).unwrap();
|
|
session.payload[0] ^= 1;
|
|
session.acknowledge().unwrap();
|
|
});
|
|
|
|
let error = server
|
|
.authenticate_and_send(expected, b"authenticated-session", Duration::from_secs(2))
|
|
.unwrap_err();
|
|
client.join().unwrap();
|
|
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
|
|
assert!(error.to_string().contains("does not match"));
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn real_windows_pipe_rejects_tampered_acknowledgement_mac() {
|
|
let session_id = format!("test-{}-3", std::process::id());
|
|
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
|
|
let key = URL_SAFE_NO_PAD
|
|
.decode(bootstrap.encoded_key().as_bytes())
|
|
.unwrap();
|
|
let pipe_name = bootstrap.pipe_name().to_owned();
|
|
let expected = current_process_identity().unwrap();
|
|
let client = std::thread::spawn(move || {
|
|
let mut session =
|
|
receive_secure_pipe_session_with_key(&pipe_name, Zeroizing::new(key)).unwrap();
|
|
session.key[0] ^= 1;
|
|
session.acknowledge().unwrap();
|
|
});
|
|
|
|
let error = server
|
|
.authenticate_and_send(expected, b"authenticated-session", Duration::from_secs(2))
|
|
.unwrap_err();
|
|
client.join().unwrap();
|
|
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
|
|
assert!(error.to_string().contains("MAC was rejected"));
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn real_windows_pipe_times_out_after_connected_client_stalls() {
|
|
let session_id = format!("test-{}-4", std::process::id());
|
|
let (server, bootstrap) = SecurePipeServer::create(&session_id).unwrap();
|
|
let pipe_name = bootstrap.pipe_name().to_owned();
|
|
let expected = current_process_identity().unwrap();
|
|
let (connected_sender, connected_receiver) = std::sync::mpsc::channel();
|
|
let client = std::thread::spawn(move || {
|
|
let _pipe = windows_platform::connect_client(&pipe_name, Duration::from_secs(2))
|
|
.expect("test client connects");
|
|
connected_sender.send(()).unwrap();
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
});
|
|
connected_receiver
|
|
.recv_timeout(Duration::from_secs(2))
|
|
.unwrap();
|
|
|
|
let error = server
|
|
.authenticate_and_send(
|
|
expected,
|
|
b"authenticated-session",
|
|
Duration::from_millis(100),
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
|
|
client.join().unwrap();
|
|
}
|
|
}
|