diff --git a/crates/rrg/Cargo.toml b/crates/rrg/Cargo.toml index ac175e51..c0e4c582 100644 --- a/crates/rrg/Cargo.toml +++ b/crates/rrg/Cargo.toml @@ -22,6 +22,7 @@ default = [ "action-list_winreg_values", "action-list_winreg_keys", "action-query_wmi", + "action-execute_signed_command", ] action-get_system_metadata = [] @@ -39,6 +40,7 @@ action-get_winreg_value = [] action-list_winreg_values = [] action-list_winreg_keys = [] action-query_wmi = [] +action-execute_signed_command = [] test-setfattr = [] test-chattr = [] @@ -130,3 +132,12 @@ features = [ "Win32_Foundation", "Win32_Storage_FileSystem", ] + +[dependencies.ed25519-dalek] +version = "2.1.1" +features = [ + "rand_core", +] + +[dependencies.hex] +version = "0.4.3" diff --git a/crates/rrg/src/action.rs b/crates/rrg/src/action.rs index 23db0798..16e0fd95 100644 --- a/crates/rrg/src/action.rs +++ b/crates/rrg/src/action.rs @@ -51,6 +51,9 @@ pub mod list_winreg_keys; #[cfg(feature = "action-query_wmi")] pub mod query_wmi; +#[cfg(feature = "action-execute_signed_command")] +pub mod execute_signed_command; + use log::info; /// Dispatches the given `request` to an appropriate action handler. @@ -125,6 +128,10 @@ where QueryWmi => { handle(session, request, self::query_wmi::handle) } + #[cfg(feature = "action-execute_signed_command")] + ExecuteSignedCommand => { + handle(session, request, self::execute_signed_command::handle) + } // We allow `unreachable_patterns` because otherwise we get a warning if // we compile with all the actions enabled. #[allow(unreachable_patterns)] diff --git a/crates/rrg/src/action/execute_signed_command.rs b/crates/rrg/src/action/execute_signed_command.rs new file mode 100644 index 00000000..f6e1b7c7 --- /dev/null +++ b/crates/rrg/src/action/execute_signed_command.rs @@ -0,0 +1,117 @@ +// Copyright 2024 Google LLC +// +// Use of this source code is governed by an MIT-style license that can be found +// in the LICENSE file or at https://opensource.org/licenses/MIT. + +use std::process::ExitStatus; + +use protobuf::Message; + +/// Arguments of the `execute_signed_command` action. +pub struct Args { + raw_command: Vec, + command: rrg_proto::execute_signed_command::SignedCommand, + stdin: Stdin, + ed25519_signature: ed25519_dalek::Signature, + timeout: std::time::Duration, +} + +/// Result of the `execute_signed_command` action. +pub struct Item { + /// Exit status of the command subprocess. + exit_status: ExitStatus, + /// Standard output of the command executiom. + stdout: Vec, + /// Wheather standard output is truncated. + truncated_stdout: bool, + /// Standard error of the command executiom. + stderr: Vec, + /// Wheather stderr is truncated. + truncated_stderr: bool, +} + +enum Stdin { + NONE, + UNSIGNED(Vec), + SIGNED(Vec), +} + + +/// Handles invocations of the `execute_signed_command` action. +pub fn handle(session: &mut S, mut args: Args) -> crate::session::Result<()> +where + S: crate::session::Session, +{ + // TODO(s-westphal): Add implementation. + Ok(()) +} + +impl crate::request::Args for Args { + + type Proto = rrg_proto::execute_signed_command::Args; + + fn from_proto(mut proto: Self::Proto) -> Result { + use crate::request::ParseArgsError; + + let raw_signature= proto.take_command_ed25519_signature(); + + let ed25519_signature = ed25519_dalek::Signature::try_from(&raw_signature[..]) + .map_err(|error| ParseArgsError::invalid_field("command_ed25519_signature", error))?; + + let raw_command = proto.take_command(); + let mut command = rrg_proto::execute_signed_command::SignedCommand::parse_from_bytes(&raw_command) + .map_err(|error| ParseArgsError::invalid_field("command", error))?; + + + let stdin: Stdin; + if command.has_signed_stdin() { + stdin = Stdin::SIGNED(command.take_signed_stdin()); + } else if command.unsigned_stdin() && !proto.unsigned_stdin.is_empty() { + stdin = Stdin::UNSIGNED(proto.take_unsigned_stdin()); + } else { + stdin = Stdin::NONE + } + + let timeout = std::time::Duration::try_from(proto.take_timeout()) + .map_err(|error| ParseArgsError::invalid_field("command", error))?; + + Ok(Args { + raw_command, + command, + ed25519_signature, + stdin, + timeout, + }) + } +} + +impl crate::response::Item for Item { + + type Proto = rrg_proto::execute_signed_command::Result; + + fn into_proto(self) -> Self::Proto { + + let mut proto = rrg_proto::execute_signed_command::Result::new(); + + if let Some(exit_code) = self.exit_status.code() { + proto.set_exit_code(exit_code); + } + + #[cfg(target_family = "unix")] + { + use std::os::unix::process::ExitStatusExt; + + if let Some(exit_signal) = self.exit_status.signal() { + proto.set_exit_signal(exit_signal); + } + } + + proto.set_stdout(self.stdout); + proto.set_stdout_truncated(self.truncated_stdout); + + proto.set_stderr(self.stderr); + proto.set_stderr_truncated(self.truncated_stderr); + + proto + } +} \ No newline at end of file diff --git a/crates/rrg/src/args.rs b/crates/rrg/src/args.rs index a5dfbcfa..a32719c1 100644 --- a/crates/rrg/src/args.rs +++ b/crates/rrg/src/args.rs @@ -49,6 +49,14 @@ pub struct Args { arg_name="PATH", description="whether to log to a file")] pub log_to_file: Option, + + /// The public key for verfying signed commands. + #[argh(option, + long="command-verification-key", + arg_name="KEY", + description="verification key for signed commands", + from_str_fn(parse_verfication_key))] + pub command_verification_key: Option, } /// Parses command-line arguments. @@ -66,3 +74,10 @@ pub fn from_env_args() -> Args { fn parse_duration(value: &str) -> Result { humantime::parse_duration(value).map_err(|error| error.to_string()) } + +/// Parses a ed25519 verification key from hex data given as string to a `VerifyingKey` object. +fn parse_verfication_key(key: &str) -> Result { + let bytes = hex::decode(key).map_err(|error| error.to_string())?; + ed25519_dalek::VerifyingKey::try_from(&bytes[..]) + .map_err(|error| error.to_string()) +} diff --git a/crates/rrg/src/request.rs b/crates/rrg/src/request.rs index 365cd1d5..73fb3517 100644 --- a/crates/rrg/src/request.rs +++ b/crates/rrg/src/request.rs @@ -47,6 +47,8 @@ pub enum Action { ListWinregKeys, /// Query WMI using WQL (Windows-only). QueryWmi, + /// Execute a signed command. + ExecuteSignedCommand, } impl std::fmt::Display for Action { @@ -70,6 +72,7 @@ impl std::fmt::Display for Action { Action::ListWinregValues => write!(fmt, "list_winreg_values"), Action::ListWinregKeys => write!(fmt, "list_winreg_keys"), Action::QueryWmi => write!(fmt, "query_wmi"), + Action::ExecuteSignedCommand => write!(fmt, "execute_signed_command"), } } } @@ -119,6 +122,7 @@ impl TryFrom for Action { LIST_WINREG_VALUES => Ok(Action::ListWinregValues), LIST_WINREG_KEYS => Ok(Action::ListWinregKeys), QUERY_WMI => Ok(Action::QueryWmi), + EXECUTE_SIGNED_COMMAND => Ok(Action::ExecuteSignedCommand), _ => { let value = protobuf::Enum::value(&proto); Err(UnknownAction { value }) diff --git a/crates/rrg/src/session/fake.rs b/crates/rrg/src/session/fake.rs index 0b617c0d..cbf7bcce 100644 --- a/crates/rrg/src/session/fake.rs +++ b/crates/rrg/src/session/fake.rs @@ -23,6 +23,7 @@ impl FakeSession { pub fn new() -> FakeSession { FakeSession::with_args(crate::args::Args { heartbeat_rate: std::time::Duration::from_secs(0), + command_verification_key: Some(ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng).verifying_key()), verbosity: log::LevelFilter::Debug, log_to_stdout: false, log_to_file: None, diff --git a/proto/rrg.proto b/proto/rrg.proto index ded2ffde..4f40993f 100644 --- a/proto/rrg.proto +++ b/proto/rrg.proto @@ -48,6 +48,8 @@ enum Action { QUERY_WMI = 16; /// Grep the specified file for a pattern. GREP_FILE_CONTENTS = 17; + /// Execute a signed command. + EXECUTE_SIGNED_COMMAND = 18; // TODO: Define more actions that should be supported.