From 32c499bbfc665aad080b368ae8686d370064fade Mon Sep 17 00:00:00 2001 From: Mitchell Grenier Date: Tue, 10 Mar 2026 16:14:03 -0700 Subject: [PATCH 1/2] Add constraint parsing to RusticaAgent --- Cargo.lock | 4 +-- rustica-agent/Cargo.toml | 2 +- rustica-agent/src/lib.rs | 15 +++++++++++ rustica-agent/src/sshagent/constraints.rs | 32 +++++++++++++++++++++++ rustica-agent/src/sshagent/handler.rs | 14 ++++++++++ rustica-agent/src/sshagent/mod.rs | 9 ++++--- rustica-agent/src/sshagent/protocol.rs | 28 ++++++++++++++++++-- rustica/Cargo.toml | 2 +- 8 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 rustica-agent/src/sshagent/constraints.rs diff --git a/Cargo.lock b/Cargo.lock index 20bc0235..68be9ae1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4779,9 +4779,9 @@ dependencies = [ [[package]] name = "sshcerts" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86255551f89d85d725a8aa6c795e87f582c4a152563defec247f76600416ee" +checksum = "77fb95516e393037486aaa0dddf65fd6ada488d274fb3914204706dea7c60b32" dependencies = [ "aes 0.7.5", "authenticator", diff --git a/rustica-agent/Cargo.toml b/rustica-agent/Cargo.toml index acabe0b1..90311dfc 100644 --- a/rustica-agent/Cargo.toml +++ b/rustica-agent/Cargo.toml @@ -24,7 +24,7 @@ serde = "1.0.97" serde_derive = "1.0" sha2 = "0.9.2" # For Production -sshcerts = { version = "0.14.0" } +sshcerts = { version = "0.14.1" } # For Development # sshcerts = { path = "../../sshcerts", features = [ # "yubikey-support", diff --git a/rustica-agent/src/lib.rs b/rustica-agent/src/lib.rs index f8b08fd9..0f3afc85 100644 --- a/rustica-agent/src/lib.rs +++ b/rustica-agent/src/lib.rs @@ -10,6 +10,7 @@ use async_trait::async_trait; use rustica::key::U2FAttestation; use config::{Options, UpdatableConfiguration}; +use sshagent::constraints::Constraint; pub use config::Config; use serde_derive::{Deserialize, Serialize}; @@ -301,6 +302,20 @@ impl SshAgentHandler for Handler { Ok(Response::Success) } + async fn add_identity_constrained( + &self, + private_key: PrivateKey, + constraints: Vec, + ) -> Result { + trace!("Add Identity Constrained call"); + for constraint in &constraints { + trace!("Unused Constraint: {:?}", constraint); + } + let public_key = private_key.pubkey.encode(); + self.identities.lock().await.insert(public_key, private_key); + Ok(Response::Success) + } + async fn identities(&self) -> Result { trace!("Identities call"); // We start building identies with the manually loaded keys diff --git a/rustica-agent/src/sshagent/constraints.rs b/rustica-agent/src/sshagent/constraints.rs new file mode 100644 index 00000000..63be31f7 --- /dev/null +++ b/rustica-agent/src/sshagent/constraints.rs @@ -0,0 +1,32 @@ +use sshcerts::ssh::Reader; + +#[derive(Debug)] +pub enum Constraint { + Lifetime(u32), + Confirm, + Extension(String, Vec), +} + +pub fn parse_constraints(buf: &[u8]) -> Result, String> { + let mut constraints = Vec::new(); + let mut reader = Reader::new(buf); + let total_bytes = buf.len(); + while reader.get_offset() < total_bytes { + let constraint_type = reader.read_raw_bytes(1).map_err(|e| e.to_string())?[0]; + match constraint_type { + 1 => { + constraints.push(Constraint::Lifetime( + reader.read_u32().map_err(|e| e.to_string())?, + )); + } + 2 => constraints.push(Constraint::Confirm), + 255 => { + let ext_name = reader.read_string().map_err(|e| e.to_string())?; + let ext_data = reader.read_bytes().map_err(|e| e.to_string())?; + constraints.push(Constraint::Extension(ext_name, ext_data)); + } + _ => return Err(format!("Unknown constraint type: {}", constraint_type)), + } + } + Ok(constraints) +} diff --git a/rustica-agent/src/sshagent/handler.rs b/rustica-agent/src/sshagent/handler.rs index d36a3fed..c573409f 100644 --- a/rustica-agent/src/sshagent/handler.rs +++ b/rustica-agent/src/sshagent/handler.rs @@ -1,3 +1,5 @@ +use crate::sshagent::constraints::Constraint; + use super::protocol::Request; use super::protocol::Response; @@ -9,6 +11,11 @@ use sshcerts::PrivateKey; #[async_trait] pub trait SshAgentHandler: Send + Sync { async fn add_identity(&self, key: PrivateKey) -> HandleResult; + async fn add_identity_constrained( + &self, + key: PrivateKey, + constraints: Vec, + ) -> HandleResult; async fn identities(&self) -> HandleResult; async fn sign_request( &self, @@ -29,6 +36,13 @@ pub trait SshAgentHandler: Send + Sync { .await } Request::AddIdentity { private_key } => self.add_identity(private_key).await, + Request::AddIdentityConstrained { + private_key, + constraints, + } => { + self.add_identity_constrained(private_key, constraints) + .await + } Request::Unknown => Ok(Response::Failure), } } diff --git a/rustica-agent/src/sshagent/mod.rs b/rustica-agent/src/sshagent/mod.rs index d061a88d..1064a414 100644 --- a/rustica-agent/src/sshagent/mod.rs +++ b/rustica-agent/src/sshagent/mod.rs @@ -1,11 +1,12 @@ extern crate byteorder; mod agent; -mod protocol; -mod handler; +pub mod constraints; pub mod error; +mod handler; +mod protocol; -pub use handler::SshAgentHandler; pub use agent::Agent; +pub use handler::SshAgentHandler; +pub use protocol::Identity; pub use protocol::Response; -pub use protocol::Identity; \ No newline at end of file diff --git a/rustica-agent/src/sshagent/protocol.rs b/rustica-agent/src/sshagent/protocol.rs index 50c7c46a..623e04ea 100644 --- a/rustica-agent/src/sshagent/protocol.rs +++ b/rustica-agent/src/sshagent/protocol.rs @@ -1,4 +1,5 @@ use byteorder::{BigEndian, WriteBytesExt}; +use sshcerts::ssh::Reader; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::UnixStream, @@ -31,11 +32,11 @@ impl MessageRequest { 17 => MessageRequest::AddIdentity, 18 => MessageRequest::RemoveIdentity, 19 => MessageRequest::RemoveAllIdentities, - 25 => MessageRequest::AddIdConstrained, 20 => MessageRequest::AddSmartcardKey, 21 => MessageRequest::RemoveSmartcardKey, 22 => MessageRequest::Lock, 23 => MessageRequest::Unlock, + 25 => MessageRequest::AddIdConstrained, 26 => MessageRequest::AddSmartcardKeyConstrained, 27 => MessageRequest::Extension, _ => MessageRequest::Unknown, @@ -76,6 +77,10 @@ pub enum Request { AddIdentity { private_key: sshcerts::PrivateKey, }, + AddIdentityConstrained { + private_key: sshcerts::PrivateKey, + constraints: Vec, + }, Unknown, } @@ -99,7 +104,26 @@ impl Request { }, MessageRequest::RemoveIdentity => Ok(Request::Unknown), MessageRequest::RemoveAllIdentities => Ok(Request::Unknown), - MessageRequest::AddIdConstrained => Ok(Request::Unknown), + MessageRequest::AddIdConstrained => { + let mut reader = Reader::new(buf); + + let private_key = match sshcerts::PrivateKey::read_private_key(&mut reader) { + Ok(private_key) => private_key, + Err(_) => return Ok(Request::Unknown), + }; + + let mut constraints_buf = &buf[reader.get_offset()..]; + println!("Constraints buffer: {:?}", constraints_buf); + let constraints = match super::constraints::parse_constraints(&mut constraints_buf) + { + Ok(constraints) => constraints, + Err(_) => return Ok(Request::Unknown), + }; + Ok(Request::AddIdentityConstrained { + private_key, + constraints, + }) + } MessageRequest::AddSmartcardKey => Ok(Request::Unknown), MessageRequest::RemoveSmartcardKey => Ok(Request::Unknown), MessageRequest::Lock => Ok(Request::Unknown), diff --git a/rustica/Cargo.toml b/rustica/Cargo.toml index d466f130..480ea140 100644 --- a/rustica/Cargo.toml +++ b/rustica/Cargo.toml @@ -41,7 +41,7 @@ serde = { version = "1.0", features = ["derive"] } # "yubikey-lite", # ] } # For Development -sshcerts = { version = "0.14.0", default-features = false, features = [ +sshcerts = { version = "0.14.1", default-features = false, features = [ "fido-lite", "x509-support", "yubikey-lite", From d47c815d540e562a18e28d807bb22df636ad19b5 Mon Sep 17 00:00:00 2001 From: Mitchell Grenier Date: Tue, 10 Mar 2026 16:33:51 -0700 Subject: [PATCH 2/2] Fix extra prints and better errors --- rustica-agent/src/lib.rs | 4 ++-- rustica-agent/src/sshagent/constraints.rs | 22 ++++++++++++++++------ rustica-agent/src/sshagent/protocol.rs | 6 ++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/rustica-agent/src/lib.rs b/rustica-agent/src/lib.rs index 0f3afc85..99052c5b 100644 --- a/rustica-agent/src/lib.rs +++ b/rustica-agent/src/lib.rs @@ -308,8 +308,8 @@ impl SshAgentHandler for Handler { constraints: Vec, ) -> Result { trace!("Add Identity Constrained call"); - for constraint in &constraints { - trace!("Unused Constraint: {:?}", constraint); + if !constraints.is_empty() { + trace!("Key is being added with constraints"); } let public_key = private_key.pubkey.encode(); self.identities.lock().await.insert(public_key, private_key); diff --git a/rustica-agent/src/sshagent/constraints.rs b/rustica-agent/src/sshagent/constraints.rs index 63be31f7..e798ffdf 100644 --- a/rustica-agent/src/sshagent/constraints.rs +++ b/rustica-agent/src/sshagent/constraints.rs @@ -1,5 +1,7 @@ use sshcerts::ssh::Reader; +use crate::sshagent::error::ParsingError; + #[derive(Debug)] pub enum Constraint { Lifetime(u32), @@ -7,25 +9,33 @@ pub enum Constraint { Extension(String, Vec), } -pub fn parse_constraints(buf: &[u8]) -> Result, String> { +pub fn parse_constraints(buf: &[u8]) -> ParsingError> { let mut constraints = Vec::new(); let mut reader = Reader::new(buf); let total_bytes = buf.len(); while reader.get_offset() < total_bytes { - let constraint_type = reader.read_raw_bytes(1).map_err(|e| e.to_string())?[0]; + let constraint_type = reader + .read_raw_bytes(1) + .map_err(|_| "Failed to read constraint type")?[0]; match constraint_type { 1 => { constraints.push(Constraint::Lifetime( - reader.read_u32().map_err(|e| e.to_string())?, + reader + .read_u32() + .map_err(|_| "Failed to read u32 for lifetime")?, )); } 2 => constraints.push(Constraint::Confirm), 255 => { - let ext_name = reader.read_string().map_err(|e| e.to_string())?; - let ext_data = reader.read_bytes().map_err(|e| e.to_string())?; + let ext_name = reader + .read_string() + .map_err(|_| "Failed to read string for extension name")?; + let ext_data = reader + .read_bytes() + .map_err(|_| "Failed to read bytes for extension data")?; constraints.push(Constraint::Extension(ext_name, ext_data)); } - _ => return Err(format!("Unknown constraint type: {}", constraint_type)), + _ => return Err("Unknown constraint type".into()), } } Ok(constraints) diff --git a/rustica-agent/src/sshagent/protocol.rs b/rustica-agent/src/sshagent/protocol.rs index 623e04ea..52603891 100644 --- a/rustica-agent/src/sshagent/protocol.rs +++ b/rustica-agent/src/sshagent/protocol.rs @@ -112,10 +112,8 @@ impl Request { Err(_) => return Ok(Request::Unknown), }; - let mut constraints_buf = &buf[reader.get_offset()..]; - println!("Constraints buffer: {:?}", constraints_buf); - let constraints = match super::constraints::parse_constraints(&mut constraints_buf) - { + let constraints_buf = &buf[reader.get_offset()..]; + let constraints = match super::constraints::parse_constraints(&constraints_buf) { Ok(constraints) => constraints, Err(_) => return Ok(Request::Unknown), };