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