Skip to content

Commit f35429b

Browse files
authored
faux-mgs: tech port unlock with permslip (#434)
1 parent a8e4c63 commit f35429b

File tree

1 file changed

+150
-79
lines changed

1 file changed

+150
-79
lines changed

faux-mgs/src/main.rs

Lines changed: 150 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use futures::StreamExt;
1818
use gateway_messages::ignition::TransceiverSelect;
1919
use gateway_messages::ComponentAction;
2020
use gateway_messages::ComponentActionResponse;
21+
use gateway_messages::EcdsaSha2Nistp256Challenge;
2122
use gateway_messages::IgnitionCommand;
2223
use gateway_messages::LedComponentAction;
2324
use gateway_messages::MonorailComponentAction;
@@ -555,17 +556,29 @@ enum MonorailCommand {
555556
#[clap(flatten)]
556557
cmd: UnlockGroup,
557558

558-
/// Public key for SSH signing challenge
559+
/// Name of the signing key for producing unlock challenge responses
559560
///
560-
/// This is either a path to a public key (ending in `.pub`), or a
561-
/// substring to match against known keys (which can be printed with
562-
/// `faux-mgs monorail unlock --list`).
561+
/// This is either a path to an SSH public key file (ending in `.pub`),
562+
/// or a substring to match against known SSH keys (which can be printed
563+
/// with `faux-mgs monorail unlock --list`), or a permslip key name (see
564+
/// `permslip list-keys -t`).
563565
#[clap(short, long, conflicts_with = "list")]
564566
key: Option<String>,
565567

566568
/// Path to the SSH agent socket
567569
#[clap(long, env)]
568570
ssh_auth_sock: Option<PathBuf>,
571+
572+
/// Use the Online Signing Service with `permslip`
573+
#[clap(
574+
short,
575+
long,
576+
alias = "online",
577+
conflicts_with = "list",
578+
conflicts_with = "ssh_auth_sock",
579+
requires = "key"
580+
)]
581+
permslip: bool,
569582
},
570583

571584
/// Lock the technician port
@@ -1627,6 +1640,7 @@ async fn run_command(
16271640
cmd: UnlockGroup { time, list },
16281641
key,
16291642
ssh_auth_sock,
1643+
permslip,
16301644
} => {
16311645
if list {
16321646
let Some(ssh_auth_sock) = ssh_auth_sock else {
@@ -1646,6 +1660,7 @@ async fn run_command(
16461660
time_sec,
16471661
ssh_auth_sock,
16481662
key,
1663+
permslip,
16491664
)
16501665
.await?;
16511666
}
@@ -1922,8 +1937,9 @@ async fn monorail_unlock(
19221937
log: &Logger,
19231938
sp: &SingleSp,
19241939
time_sec: u32,
1925-
socket: Option<PathBuf>,
1940+
ssh_sock: Option<PathBuf>,
19261941
pub_key: Option<String>,
1942+
permslip: bool,
19271943
) -> Result<()> {
19281944
let r = sp
19291945
.component_action_with_response(
@@ -1946,82 +1962,14 @@ async fn monorail_unlock(
19461962
UnlockChallenge::Trivial { timestamp } => {
19471963
UnlockResponse::Trivial { timestamp }
19481964
}
1949-
UnlockChallenge::EcdsaSha2Nistp256(data) => {
1950-
let Some(socket) = socket else {
1951-
bail!("must provide --ssh-auth-sock");
1952-
};
1953-
let keys = ssh_list_keys(&socket)?;
1954-
let pub_key = if keys.len() == 1 && pub_key.is_none() {
1955-
keys[0].clone()
1965+
UnlockChallenge::EcdsaSha2Nistp256(ecdsa_challenge) => {
1966+
if pub_key.is_some() && permslip {
1967+
unlock_permslip(log, pub_key.unwrap(), challenge)?
1968+
} else if let Some(socket) = ssh_sock {
1969+
unlock_ssh(log, socket, pub_key, ecdsa_challenge)?
19561970
} else {
1957-
let Some(pub_key) = pub_key else {
1958-
bail!(
1959-
"need --key for ECDSA challenge; \
1960-
multiple keys are available"
1961-
);
1962-
};
1963-
if pub_key.ends_with(".pub") {
1964-
ssh_key::PublicKey::read_openssh_file(Path::new(&pub_key))
1965-
.with_context(|| {
1966-
format!("could not read key from {pub_key:?}")
1967-
})?
1968-
} else {
1969-
let mut found = None;
1970-
for k in keys.iter() {
1971-
if k.to_openssh()?.contains(&pub_key) {
1972-
if found.is_some() {
1973-
bail!("multiple keys contain '{pub_key}'");
1974-
}
1975-
found = Some(k);
1976-
}
1977-
}
1978-
let Some(found) = found else {
1979-
bail!(
1980-
"could not match '{pub_key}'; \
1981-
use `faux-mgs monorail unlock --list` \
1982-
to print keys"
1983-
);
1984-
};
1985-
found.clone()
1986-
}
1987-
};
1988-
1989-
let mut data = data.as_bytes().to_vec();
1990-
let signer_nonce: [u8; 8] = rand::random();
1991-
data.extend(signer_nonce);
1992-
1993-
let signed = ssh_keygen_sign(socket, pub_key, &data)?;
1994-
debug!(log, "got signature {signed:?}");
1995-
1996-
let key_bytes =
1997-
signed.public_key().ecdsa().unwrap().as_sec1_bytes();
1998-
assert_eq!(key_bytes.len(), 65, "invalid key length");
1999-
let mut key = [0u8; 65];
2000-
key.copy_from_slice(key_bytes);
2001-
2002-
// Signature bytes are encoded per
2003-
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
2004-
//
2005-
// They are a pair of `mpint` values, per
2006-
// https://datatracker.ietf.org/doc/html/rfc4251
2007-
//
2008-
// Each one is either 32 bytes or 33 bytes with a leading zero, so
2009-
// we'll awkwardly allow for both cases.
2010-
let mut r = std::io::Cursor::new(signed.signature_bytes());
2011-
use std::io::Read;
2012-
let mut signature = [0u8; 64];
2013-
for i in 0..2 {
2014-
let mut size = [0u8; 4];
2015-
r.read_exact(&mut size)?;
2016-
match u32::from_be_bytes(size) {
2017-
32 => (),
2018-
33 => r.read_exact(&mut [0u8])?, // eat the leading byte
2019-
_ => bail!("invalid length {i}"),
2020-
}
2021-
r.read_exact(&mut signature[i * 32..][..32])?;
1971+
bail!("don't know how to unlock tech port without ssh or permslip")
20221972
}
2023-
2024-
UnlockResponse::EcdsaSha2Nistp256 { key, signer_nonce, signature }
20251973
}
20261974
};
20271975
sp.component_action(
@@ -2037,6 +1985,129 @@ async fn monorail_unlock(
20371985
Ok(())
20381986
}
20391987

1988+
fn unlock_permslip(
1989+
log: &Logger,
1990+
key_name: String,
1991+
challenge: UnlockChallenge,
1992+
) -> Result<UnlockResponse> {
1993+
use std::env;
1994+
use std::process::{Command, Stdio};
1995+
1996+
let mut permslip = Command::new(
1997+
env::var("PERMSLIP").unwrap_or_else(|_| String::from("permslip")),
1998+
)
1999+
.arg("sign")
2000+
.arg(key_name)
2001+
.arg("--sshauth")
2002+
.arg("--kind=tech-port-unlock-challenge")
2003+
.stdin(Stdio::piped())
2004+
.stdout(Stdio::piped())
2005+
.stderr(Stdio::inherit())
2006+
.spawn()
2007+
.map_err(|_| {
2008+
anyhow!(
2009+
"Unable to execute `permslip`, is it in your PATH and executable? \
2010+
You may also override it with the PERMSLIP environment variable."
2011+
)
2012+
})?;
2013+
2014+
let mut input =
2015+
permslip.stdin.take().context("can't get permslip input")?;
2016+
input.write_all(serde_json::to_string(&challenge)?.as_bytes())?;
2017+
input.flush()?;
2018+
drop(input);
2019+
2020+
let output =
2021+
permslip.wait_with_output().context("can't read permslip output")?;
2022+
if output.status.success() {
2023+
let response =
2024+
serde_json::from_slice::<UnlockResponse>(&output.stdout)?;
2025+
debug!(log, "got response from permslip"; "response" => ?response);
2026+
Ok(response)
2027+
} else {
2028+
bail!("online signing with permslip failed");
2029+
}
2030+
}
2031+
2032+
fn unlock_ssh(
2033+
log: &Logger,
2034+
socket: PathBuf,
2035+
pub_key: Option<String>,
2036+
challenge: EcdsaSha2Nistp256Challenge,
2037+
) -> Result<UnlockResponse> {
2038+
let keys = ssh_list_keys(&socket)?;
2039+
let pub_key = if keys.len() == 1 && pub_key.is_none() {
2040+
keys[0].clone()
2041+
} else {
2042+
let Some(pub_key) = pub_key else {
2043+
bail!(
2044+
"need --key for ECDSA challenge; \
2045+
multiple keys are available"
2046+
);
2047+
};
2048+
if pub_key.ends_with(".pub") {
2049+
ssh_key::PublicKey::read_openssh_file(Path::new(&pub_key))
2050+
.with_context(|| {
2051+
format!("could not read key from {pub_key:?}")
2052+
})?
2053+
} else {
2054+
let mut found = None;
2055+
for k in keys.iter() {
2056+
if k.to_openssh()?.contains(&pub_key) {
2057+
if found.is_some() {
2058+
bail!("multiple keys contain '{pub_key}'");
2059+
}
2060+
found = Some(k);
2061+
}
2062+
}
2063+
let Some(found) = found else {
2064+
bail!(
2065+
"could not match '{pub_key}'; \
2066+
use `faux-mgs monorail unlock --list` \
2067+
to print keys"
2068+
);
2069+
};
2070+
found.clone()
2071+
}
2072+
};
2073+
2074+
let mut data = challenge.as_bytes().to_vec();
2075+
let signer_nonce: [u8; 8] = rand::random();
2076+
data.extend(signer_nonce);
2077+
2078+
let signed = ssh_keygen_sign(socket, pub_key, &data)?;
2079+
debug!(log, "got signature {signed:?}");
2080+
2081+
let key_bytes = signed.public_key().ecdsa().unwrap().as_sec1_bytes();
2082+
assert_eq!(key_bytes.len(), 65, "invalid key length");
2083+
let mut key = [0u8; 65];
2084+
key.copy_from_slice(key_bytes);
2085+
2086+
// Signature bytes are encoded per
2087+
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
2088+
//
2089+
// They are a pair of `mpint` values, per
2090+
// https://datatracker.ietf.org/doc/html/rfc4251
2091+
//
2092+
// Each one is either 32 bytes or 33 bytes with a leading zero, so
2093+
// we'll awkwardly allow for both cases.
2094+
let mut r = std::io::Cursor::new(signed.signature_bytes());
2095+
use std::io::Read;
2096+
let mut signature = [0u8; 64];
2097+
for i in 0..2 {
2098+
let mut size = [0u8; 4];
2099+
r.read_exact(&mut size)?;
2100+
match u32::from_be_bytes(size) {
2101+
32 => (),
2102+
33 => r.read_exact(&mut [0u8])?, // eat the leading byte
2103+
_ => bail!("invalid length {i}"),
2104+
}
2105+
r.read_exact(&mut signature[i * 32..][..32])?;
2106+
}
2107+
2108+
Ok(UnlockResponse::EcdsaSha2Nistp256 { key, signer_nonce, signature })
2109+
}
2110+
20402111
fn ssh_keygen_sign(
20412112
socket: PathBuf,
20422113
pub_key: ssh_key::PublicKey,

0 commit comments

Comments
 (0)