@@ -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- 
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- \ 
1960- 
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+ 
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+ \ 
2037+ 
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+ 
20182081fn  ssh_keygen_sign ( 
20192082    socket :  PathBuf , 
20202083    pub_key :  ssh_key:: PublicKey , 
0 commit comments