Skip to content

Commit c80fb4f

Browse files
committed
faux-mgs: tech port unlock with permslip
1 parent 4301011 commit c80fb4f

File tree

1 file changed

+142
-79
lines changed

1 file changed

+142
-79
lines changed

faux-mgs/src/main.rs

Lines changed: 142 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,28 @@ 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+
requires = "key"
579+
)]
580+
permslip: bool,
569581
},
570582

571583
/// Lock the technician port
@@ -1605,6 +1617,7 @@ async fn run_command(
16051617
cmd: UnlockGroup { time, list },
16061618
key,
16071619
ssh_auth_sock,
1620+
permslip,
16081621
} => {
16091622
if list {
16101623
let Some(ssh_auth_sock) = ssh_auth_sock else {
@@ -1624,6 +1637,7 @@ async fn run_command(
16241637
time_sec,
16251638
ssh_auth_sock,
16261639
key,
1640+
permslip,
16271641
)
16281642
.await?;
16291643
}
@@ -1900,8 +1914,9 @@ async fn monorail_unlock(
19001914
log: &Logger,
19011915
sp: &SingleSp,
19021916
time_sec: u32,
1903-
socket: Option<PathBuf>,
1917+
ssh_sock: Option<PathBuf>,
19041918
pub_key: Option<String>,
1919+
permslip: bool,
19051920
) -> Result<()> {
19061921
let r = sp
19071922
.component_action_with_response(
@@ -1924,82 +1939,14 @@ async fn monorail_unlock(
19241939
UnlockChallenge::Trivial { timestamp } => {
19251940
UnlockResponse::Trivial { timestamp }
19261941
}
1927-
UnlockChallenge::EcdsaSha2Nistp256(data) => {
1928-
let Some(socket) = socket else {
1929-
bail!("must provide --ssh-auth-sock");
1930-
};
1931-
let keys = ssh_list_keys(&socket)?;
1932-
let pub_key = if keys.len() == 1 && pub_key.is_none() {
1933-
keys[0].clone()
1942+
UnlockChallenge::EcdsaSha2Nistp256(ecdsa_challenge) => {
1943+
if pub_key.is_some() && permslip {
1944+
unlock_permslip(log, pub_key.unwrap(), challenge)?
1945+
} else if let Some(socket) = ssh_sock {
1946+
unlock_ssh(log, socket, pub_key, ecdsa_challenge)?
19341947
} else {
1935-
let Some(pub_key) = pub_key else {
1936-
bail!(
1937-
"need --key for ECDSA challenge; \
1938-
multiple keys are available"
1939-
);
1940-
};
1941-
if pub_key.ends_with(".pub") {
1942-
ssh_key::PublicKey::read_openssh_file(Path::new(&pub_key))
1943-
.with_context(|| {
1944-
format!("could not read key from {pub_key:?}")
1945-
})?
1946-
} else {
1947-
let mut found = None;
1948-
for k in keys.iter() {
1949-
if k.to_openssh()?.contains(&pub_key) {
1950-
if found.is_some() {
1951-
bail!("multiple keys contain '{pub_key}'");
1952-
}
1953-
found = Some(k);
1954-
}
1955-
}
1956-
let Some(found) = found else {
1957-
bail!(
1958-
"could not match '{pub_key}'; \
1959-
use `faux-mgs monorail unlock --list` \
1960-
to print keys"
1961-
);
1962-
};
1963-
found.clone()
1964-
}
1965-
};
1966-
1967-
let mut data = data.as_bytes().to_vec();
1968-
let signer_nonce: [u8; 8] = rand::random();
1969-
data.extend(signer_nonce);
1970-
1971-
let signed = ssh_keygen_sign(socket, pub_key, &data)?;
1972-
debug!(log, "got signature {signed:?}");
1973-
1974-
let key_bytes =
1975-
signed.public_key().ecdsa().unwrap().as_sec1_bytes();
1976-
assert_eq!(key_bytes.len(), 65, "invalid key length");
1977-
let mut key = [0u8; 65];
1978-
key.copy_from_slice(key_bytes);
1979-
1980-
// Signature bytes are encoded per
1981-
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
1982-
//
1983-
// They are a pair of `mpint` values, per
1984-
// https://datatracker.ietf.org/doc/html/rfc4251
1985-
//
1986-
// Each one is either 32 bytes or 33 bytes with a leading zero, so
1987-
// we'll awkwardly allow for both cases.
1988-
let mut r = std::io::Cursor::new(signed.signature_bytes());
1989-
use std::io::Read;
1990-
let mut signature = [0u8; 64];
1991-
for i in 0..2 {
1992-
let mut size = [0u8; 4];
1993-
r.read_exact(&mut size)?;
1994-
match u32::from_be_bytes(size) {
1995-
32 => (),
1996-
33 => r.read_exact(&mut [0u8])?, // eat the leading byte
1997-
_ => bail!("invalid length {i}"),
1998-
}
1999-
r.read_exact(&mut signature[i * 32..][..32])?;
1948+
bail!("don't know how to unlock tech port without ssh or permslip")
20001949
}
2001-
2002-
UnlockResponse::EcdsaSha2Nistp256 { key, signer_nonce, signature }
20031950
}
20041951
};
20051952
sp.component_action(
@@ -2015,6 +1962,122 @@ async fn monorail_unlock(
20151962
Ok(())
20161963
}
20171964

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

0 commit comments

Comments
 (0)