-
Notifications
You must be signed in to change notification settings - Fork 3
faux-mgs: tech port unlock with permslip #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
4301011
c691112
68df5ec
4a11780
9ddcf1e
b79fe36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ use futures::StreamExt; | |
use gateway_messages::ignition::TransceiverSelect; | ||
use gateway_messages::ComponentAction; | ||
use gateway_messages::ComponentActionResponse; | ||
use gateway_messages::EcdsaSha2Nistp256Challenge; | ||
use gateway_messages::IgnitionCommand; | ||
use gateway_messages::LedComponentAction; | ||
use gateway_messages::MonorailComponentAction; | ||
|
@@ -555,17 +556,28 @@ enum MonorailCommand { | |
#[clap(flatten)] | ||
cmd: UnlockGroup, | ||
|
||
/// Public key for SSH signing challenge | ||
/// Name of the signing key for producing unlock challenge responses | ||
/// | ||
/// This is either a path to a public key (ending in `.pub`), or a | ||
/// substring to match against known keys (which can be printed with | ||
/// `faux-mgs monorail unlock --list`). | ||
/// This is either a path to an SSH public key file (ending in `.pub`), | ||
/// or a substring to match against known SSH keys (which can be printed | ||
/// with `faux-mgs monorail unlock --list`), or a permslip key name (see | ||
/// `permslip list-keys -t`). | ||
#[clap(short, long, conflicts_with = "list")] | ||
key: Option<String>, | ||
|
||
/// Path to the SSH agent socket | ||
#[clap(long, env)] | ||
ssh_auth_sock: Option<PathBuf>, | ||
|
||
/// Use the Online Signing Service with `permslip` | ||
#[clap( | ||
short, | ||
long, | ||
alias = "online", | ||
conflicts_with = "list", | ||
requires = "key" | ||
)] | ||
permslip: bool, | ||
}, | ||
|
||
/// Lock the technician port | ||
|
@@ -1605,6 +1617,7 @@ async fn run_command( | |
cmd: UnlockGroup { time, list }, | ||
key, | ||
ssh_auth_sock, | ||
permslip, | ||
} => { | ||
if list { | ||
let Some(ssh_auth_sock) = ssh_auth_sock else { | ||
|
@@ -1624,6 +1637,7 @@ async fn run_command( | |
time_sec, | ||
ssh_auth_sock, | ||
key, | ||
permslip, | ||
) | ||
.await?; | ||
} | ||
|
@@ -1900,8 +1914,9 @@ async fn monorail_unlock( | |
log: &Logger, | ||
sp: &SingleSp, | ||
time_sec: u32, | ||
socket: Option<PathBuf>, | ||
ssh_sock: Option<PathBuf>, | ||
pub_key: Option<String>, | ||
permslip: bool, | ||
) -> Result<()> { | ||
let r = sp | ||
.component_action_with_response( | ||
|
@@ -1924,82 +1939,14 @@ async fn monorail_unlock( | |
UnlockChallenge::Trivial { timestamp } => { | ||
UnlockResponse::Trivial { timestamp } | ||
} | ||
UnlockChallenge::EcdsaSha2Nistp256(data) => { | ||
let Some(socket) = socket else { | ||
bail!("must provide --ssh-auth-sock"); | ||
}; | ||
let keys = ssh_list_keys(&socket)?; | ||
let pub_key = if keys.len() == 1 && pub_key.is_none() { | ||
keys[0].clone() | ||
UnlockChallenge::EcdsaSha2Nistp256(ecdsa_challenge) => { | ||
if pub_key.is_some() && permslip { | ||
unlock_permslip(log, pub_key.unwrap(), challenge)? | ||
} else if let Some(socket) = ssh_sock { | ||
unlock_ssh(log, socket, pub_key, ecdsa_challenge)? | ||
} else { | ||
let Some(pub_key) = pub_key else { | ||
bail!( | ||
"need --key for ECDSA challenge; \ | ||
multiple keys are available" | ||
); | ||
}; | ||
if pub_key.ends_with(".pub") { | ||
ssh_key::PublicKey::read_openssh_file(Path::new(&pub_key)) | ||
.with_context(|| { | ||
format!("could not read key from {pub_key:?}") | ||
})? | ||
} else { | ||
let mut found = None; | ||
for k in keys.iter() { | ||
if k.to_openssh()?.contains(&pub_key) { | ||
if found.is_some() { | ||
bail!("multiple keys contain '{pub_key}'"); | ||
} | ||
found = Some(k); | ||
} | ||
} | ||
let Some(found) = found else { | ||
bail!( | ||
"could not match '{pub_key}'; \ | ||
use `faux-mgs monorail unlock --list` \ | ||
to print keys" | ||
); | ||
}; | ||
found.clone() | ||
} | ||
}; | ||
|
||
let mut data = data.as_bytes().to_vec(); | ||
let signer_nonce: [u8; 8] = rand::random(); | ||
data.extend(signer_nonce); | ||
|
||
let signed = ssh_keygen_sign(socket, pub_key, &data)?; | ||
debug!(log, "got signature {signed:?}"); | ||
|
||
let key_bytes = | ||
signed.public_key().ecdsa().unwrap().as_sec1_bytes(); | ||
assert_eq!(key_bytes.len(), 65, "invalid key length"); | ||
let mut key = [0u8; 65]; | ||
key.copy_from_slice(key_bytes); | ||
|
||
// Signature bytes are encoded per | ||
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2 | ||
// | ||
// They are a pair of `mpint` values, per | ||
// https://datatracker.ietf.org/doc/html/rfc4251 | ||
// | ||
// Each one is either 32 bytes or 33 bytes with a leading zero, so | ||
// we'll awkwardly allow for both cases. | ||
let mut r = std::io::Cursor::new(signed.signature_bytes()); | ||
use std::io::Read; | ||
let mut signature = [0u8; 64]; | ||
for i in 0..2 { | ||
let mut size = [0u8; 4]; | ||
r.read_exact(&mut size)?; | ||
match u32::from_be_bytes(size) { | ||
32 => (), | ||
33 => r.read_exact(&mut [0u8])?, // eat the leading byte | ||
_ => bail!("invalid length {i}"), | ||
} | ||
r.read_exact(&mut signature[i * 32..][..32])?; | ||
bail!("don't know how to unlock tech port without ssh or permslip") | ||
} | ||
|
||
UnlockResponse::EcdsaSha2Nistp256 { key, signer_nonce, signature } | ||
} | ||
}; | ||
sp.component_action( | ||
|
@@ -2015,6 +1962,123 @@ async fn monorail_unlock( | |
Ok(()) | ||
} | ||
|
||
fn unlock_permslip( | ||
log: &Logger, | ||
key_name: String, | ||
challenge: UnlockChallenge, | ||
) -> Result<UnlockResponse> { | ||
use std::process::{Command, Stdio}; | ||
|
||
let mut permslip = Command::new("permslip") | ||
.arg("sign") | ||
.arg(key_name) | ||
.arg("--sshauth") | ||
.arg("--kind=tech-port-unlock-challenge") | ||
.stdin(Stdio::piped()) | ||
.stdout(Stdio::piped()) | ||
.stderr(Stdio::inherit()) | ||
.spawn() | ||
.context( | ||
"unable to execute `permslip`, is it in your PATH and executable?", | ||
)?; | ||
|
||
|
||
let mut input = | ||
permslip.stdin.take().context("can't get permslip input")?; | ||
input.write_all(serde_json::to_string(&challenge)?.as_bytes())?; | ||
input.flush()?; | ||
drop(input); | ||
|
||
let output = | ||
permslip.wait_with_output().context("can't read permslip output")?; | ||
if output.status.success() { | ||
let response = | ||
serde_json::from_slice::<UnlockResponse>(&output.stdout)?; | ||
debug!(log, "got response from permslip"; "response" => ?response); | ||
Ok(response) | ||
} else { | ||
bail!("online signing with permslip failed"); | ||
} | ||
} | ||
|
||
fn unlock_ssh( | ||
log: &Logger, | ||
socket: PathBuf, | ||
pub_key: Option<String>, | ||
challenge: EcdsaSha2Nistp256Challenge, | ||
) -> Result<UnlockResponse> { | ||
let keys = ssh_list_keys(&socket)?; | ||
let pub_key = if keys.len() == 1 && pub_key.is_none() { | ||
keys[0].clone() | ||
} else { | ||
let Some(pub_key) = pub_key else { | ||
bail!( | ||
"need --key for ECDSA challenge; \ | ||
multiple keys are available" | ||
); | ||
}; | ||
if pub_key.ends_with(".pub") { | ||
ssh_key::PublicKey::read_openssh_file(Path::new(&pub_key)) | ||
.with_context(|| { | ||
format!("could not read key from {pub_key:?}") | ||
})? | ||
} else { | ||
let mut found = None; | ||
for k in keys.iter() { | ||
if k.to_openssh()?.contains(&pub_key) { | ||
if found.is_some() { | ||
bail!("multiple keys contain '{pub_key}'"); | ||
} | ||
found = Some(k); | ||
} | ||
} | ||
let Some(found) = found else { | ||
bail!( | ||
"could not match '{pub_key}'; \ | ||
use `faux-mgs monorail unlock --list` \ | ||
to print keys" | ||
); | ||
}; | ||
found.clone() | ||
} | ||
}; | ||
|
||
let mut data = challenge.as_bytes().to_vec(); | ||
let signer_nonce: [u8; 8] = rand::random(); | ||
data.extend(signer_nonce); | ||
|
||
let signed = ssh_keygen_sign(socket, pub_key, &data)?; | ||
debug!(log, "got signature {signed:?}"); | ||
|
||
let key_bytes = signed.public_key().ecdsa().unwrap().as_sec1_bytes(); | ||
assert_eq!(key_bytes.len(), 65, "invalid key length"); | ||
let mut key = [0u8; 65]; | ||
key.copy_from_slice(key_bytes); | ||
|
||
// Signature bytes are encoded per | ||
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2 | ||
// | ||
// They are a pair of `mpint` values, per | ||
// https://datatracker.ietf.org/doc/html/rfc4251 | ||
// | ||
// Each one is either 32 bytes or 33 bytes with a leading zero, so | ||
// we'll awkwardly allow for both cases. | ||
let mut r = std::io::Cursor::new(signed.signature_bytes()); | ||
use std::io::Read; | ||
let mut signature = [0u8; 64]; | ||
for i in 0..2 { | ||
let mut size = [0u8; 4]; | ||
r.read_exact(&mut size)?; | ||
match u32::from_be_bytes(size) { | ||
32 => (), | ||
33 => r.read_exact(&mut [0u8])?, // eat the leading byte | ||
_ => bail!("invalid length {i}"), | ||
} | ||
r.read_exact(&mut signature[i * 32..][..32])?; | ||
} | ||
|
||
Ok(UnlockResponse::EcdsaSha2Nistp256 { key, signer_nonce, signature }) | ||
} | ||
|
||
fn ssh_keygen_sign( | ||
socket: PathBuf, | ||
pub_key: ssh_key::PublicKey, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ use crate::MessageKind; | |
use crate::MgsError; | ||
use crate::MgsRequest; | ||
use crate::MgsResponse; | ||
use crate::MonorailError; | ||
use crate::PowerState; | ||
use crate::PowerStateTransition; | ||
use crate::RotBootInfo; | ||
|
@@ -46,6 +47,8 @@ use crate::SpUpdatePrepare; | |
use crate::StartupOptions; | ||
use crate::SwitchDuration; | ||
use crate::TlvPage; | ||
use crate::UnlockChallenge; | ||
use crate::UnlockResponse; | ||
use crate::UpdateChunk; | ||
use crate::UpdateId; | ||
use crate::UpdateStatus; | ||
|
@@ -417,6 +420,15 @@ pub trait SpHandler { | |
fn start_host_flash_hash(&mut self, slot: u16) -> Result<(), SpError>; | ||
|
||
fn get_host_flash_hash(&mut self, slot: u16) -> Result<[u8; 32], SpError>; | ||
|
||
/// Unlocks the tech port if the challenge and response are compatible | ||
fn unlock( | ||
&mut self, | ||
vid: Self::VLanId, | ||
challenge: UnlockChallenge, | ||
response: UnlockResponse, | ||
time_sec: u32, | ||
) -> Result<(), MonorailError>; | ||
|
||
} | ||
|
||
/// Handle a single incoming message. | ||
|
@@ -1457,6 +1469,16 @@ mod tests { | |
) -> Result<[u8; 32], SpError> { | ||
unimplemented!() | ||
} | ||
|
||
fn unlock( | ||
&mut self, | ||
_vid: Self::VLanId, | ||
_challenge: UnlockChallenge, | ||
_response: UnlockResponse, | ||
_time_sec: u32, | ||
) -> Result<(), MonorailError> { | ||
unimplemented!() | ||
} | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make
permslip
conflict withssh_auth_sock
at the clap level?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing, added in b79fe36.