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