Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 141 additions & 1 deletion rustica-agent/src/sshagent/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,64 @@ use tokio::{
net::UnixStream,
};

use sshcerts::ssh::Reader;

use super::error::{ParsingError, WrittingError};

/// Parsed SSH agent key constraint.
#[derive(Debug, PartialEq)]
#[allow(dead_code)]
enum KeyConstraint {
Lifetime(u32),
Confirm,
Extension { name: String, data: Vec<u8> },
Unknown(u8),
}

/// Parse constraints from an AddIdConstrained message body.
///
/// Skips past the private key fields and comment, then reads the trailing constraint bytes.
fn parse_constraints(buf: &[u8]) -> Option<Vec<KeyConstraint>> {
enum F { Str, Byte }
use F::*;

let mut r = Reader::new(buf);
let key_type = r.read_string().ok()?;

// Field layout between key-type string and comment
let fields: &[F] = match key_type.as_str() {
"ssh-rsa" => &[Str, Str, Str, Str, Str, Str],
"ssh-ed25519" => &[Str, Str],
t if t.starts_with("ecdsa-sha2-") => &[Str, Str, Str],
"sk-ssh-ed25519@openssh.com" => &[Str, Str, Byte, Str, Str],
"sk-ecdsa-sha2-nistp256@openssh.com" => &[Str, Str, Str, Byte, Str, Str],
_ => return None,
};

for f in fields {
match f {
Str => { r.read_bytes().ok()?; }
Byte => { r.read_raw_bytes(1).ok()?; }
}
}
r.read_string().ok()?; // comment

let mut r = Reader::new(&buf[r.get_offset()..]);
let mut constraints = Vec::new();
while let Ok(ty) = r.read_raw_bytes(1).map(|b| b[0]) {
match ty {
1 => constraints.push(KeyConstraint::Lifetime(r.read_u32().ok()?)),
2 => constraints.push(KeyConstraint::Confirm),
255 => constraints.push(KeyConstraint::Extension {
name: r.read_string().ok()?,
data: r.read_bytes().ok()?,
}),
_ => { constraints.push(KeyConstraint::Unknown(ty)); break; }
}
}
Some(constraints)
}

#[derive(Debug, Copy, Clone)]
enum MessageRequest {
Identities,
Expand Down Expand Up @@ -99,7 +155,15 @@ impl Request {
},
MessageRequest::RemoveIdentity => Ok(Request::Unknown),
MessageRequest::RemoveAllIdentities => Ok(Request::Unknown),
MessageRequest::AddIdConstrained => Ok(Request::Unknown),
MessageRequest::AddIdConstrained => {
if let Some(constraints) = parse_constraints(buf) {
debug!("AddIdConstrained constraints: {:?}", constraints);
}
match sshcerts::PrivateKey::from_bytes(buf) {
Ok(private_key) => Ok(Request::AddIdentity { private_key }),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a trait member AddIdentityConstrained. That way we can pass constraints in properly. Then the parsing and debug printing can happen in there.

Err(_) => Ok(Request::Unknown),
}
}
MessageRequest::AddSmartcardKey => Ok(Request::Unknown),
MessageRequest::RemoveSmartcardKey => Ok(Request::Unknown),
MessageRequest::Lock => Ok(Request::Unknown),
Expand Down Expand Up @@ -166,3 +230,79 @@ impl Response {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use byteorder::{BigEndian, WriteBytesExt};

// Captured from: ssh-add -t 3600 -c /tmp/test_ed25519 (comment "test-key")
// Message type 25 (SSH_AGENTC_ADD_ID_CONSTRAINED), body only (no msg type byte).
const REAL_ADD_ID_CONSTRAINED: &[u8] = &[
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Can you provide more details on the command that generated this? As well as do a test with an SK key so we can test the more common use case?

// key_type: "ssh-ed25519" (len=11)
0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39,
// pubkey (len=32)
0x00, 0x00, 0x00, 0x20,
0x39, 0x2c, 0xfb, 0x01, 0xe0, 0xa3, 0x41, 0x66, 0x08, 0x9b, 0x88, 0x81, 0xa9, 0x65, 0x7a, 0xa9,
0x49, 0x88, 0x14, 0xc2, 0x2d, 0x7a, 0xe0, 0xd6, 0x8f, 0xa0, 0x8c, 0xfa, 0xac, 0xd5, 0x44, 0x1c,
// private key (len=64)
0x00, 0x00, 0x00, 0x40,
0x62, 0x6c, 0xe7, 0x5a, 0xe2, 0x8b, 0x18, 0xf6, 0xaa, 0x2b, 0xf1, 0x70, 0x79, 0x69, 0x0f, 0x79,
0x93, 0x70, 0x0f, 0x1b, 0xd3, 0x85, 0xec, 0xb9, 0x8f, 0x0c, 0x1e, 0x4e, 0xb7, 0xdb, 0x48, 0xee,
0x39, 0x2c, 0xfb, 0x01, 0xe0, 0xa3, 0x41, 0x66, 0x08, 0x9b, 0x88, 0x81, 0xa9, 0x65, 0x7a, 0xa9,
0x49, 0x88, 0x14, 0xc2, 0x2d, 0x7a, 0xe0, 0xd6, 0x8f, 0xa0, 0x8c, 0xfa, 0xac, 0xd5, 0x44, 0x1c,
// comment: "test-key" (len=8)
0x00, 0x00, 0x00, 0x08, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79,
// constraints: Lifetime(3600) + Confirm
0x01, 0x00, 0x00, 0x0e, 0x10,
0x02,
];

#[test]
fn parse_real_ssh_add_message() {
let constraints = parse_constraints(REAL_ADD_ID_CONSTRAINED).unwrap();
assert_eq!(constraints, vec![
KeyConstraint::Lifetime(3600),
KeyConstraint::Confirm,
]);
}

#[test]
fn real_message_key_parses_with_sshcerts() {
// Verify sshcerts can also parse the key portion of the captured message
let key = sshcerts::PrivateKey::from_bytes(REAL_ADD_ID_CONSTRAINED).unwrap();
assert_eq!(key.comment, "test-key");
}

#[test]
fn parse_no_constraints() {
// Same real key data but without the trailing constraint bytes
let buf = &REAL_ADD_ID_CONSTRAINED[..REAL_ADD_ID_CONSTRAINED.len() - 6];
let constraints = parse_constraints(buf).unwrap();
assert!(constraints.is_empty());
}

#[test]
fn parse_extension_constraint() {
let mut buf = REAL_ADD_ID_CONSTRAINED[..REAL_ADD_ID_CONSTRAINED.len() - 6].to_vec();
buf.push(255); // Extension type
WriteBytesExt::write_u32::<BigEndian>(&mut buf, 15).unwrap();
buf.extend_from_slice(b"foo@example.com");
WriteBytesExt::write_u32::<BigEndian>(&mut buf, 3).unwrap();
buf.extend_from_slice(b"\x01\x02\x03");

let constraints = parse_constraints(&buf).unwrap();
assert_eq!(constraints, vec![KeyConstraint::Extension {
name: "foo@example.com".into(),
data: vec![0x01, 0x02, 0x03],
}]);
}

#[test]
fn parse_unknown_key_type_returns_none() {
let mut buf = Vec::new();
WriteBytesExt::write_u32::<BigEndian>(&mut buf, 7).unwrap();
buf.extend_from_slice(b"unknown");
assert!(parse_constraints(&buf).is_none());
}
}