@@ -18,6 +18,7 @@ use futures::StreamExt;
1818use gateway_messages:: ignition:: TransceiverSelect ;
1919use gateway_messages:: ComponentAction ;
2020use gateway_messages:: ComponentActionResponse ;
21+ use gateway_messages:: EcdsaSha2Nistp256Challenge ;
2122use gateway_messages:: IgnitionCommand ;
2223use gateway_messages:: LedComponentAction ;
2324use 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+
20402111fn ssh_keygen_sign (
20412112 socket : PathBuf ,
20422113 pub_key : ssh_key:: PublicKey ,
0 commit comments