@@ -18,6 +18,7 @@ use futures::StreamExt;
18
18
use gateway_messages:: ignition:: TransceiverSelect ;
19
19
use gateway_messages:: ComponentAction ;
20
20
use gateway_messages:: ComponentActionResponse ;
21
+ use gateway_messages:: EcdsaSha2Nistp256Challenge ;
21
22
use gateway_messages:: IgnitionCommand ;
22
23
use gateway_messages:: LedComponentAction ;
23
24
use gateway_messages:: MonorailComponentAction ;
@@ -555,17 +556,29 @@ enum MonorailCommand {
555
556
#[ clap( flatten) ]
556
557
cmd : UnlockGroup ,
557
558
558
- /// Public key for SSH signing challenge
559
+ /// Name of the signing key for producing unlock challenge responses
559
560
///
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`).
563
565
#[ clap( short, long, conflicts_with = "list" ) ]
564
566
key : Option < String > ,
565
567
566
568
/// Path to the SSH agent socket
567
569
#[ clap( long, env) ]
568
570
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 ,
569
582
} ,
570
583
571
584
/// Lock the technician port
@@ -1627,6 +1640,7 @@ async fn run_command(
1627
1640
cmd : UnlockGroup { time, list } ,
1628
1641
key,
1629
1642
ssh_auth_sock,
1643
+ permslip,
1630
1644
} => {
1631
1645
if list {
1632
1646
let Some ( ssh_auth_sock) = ssh_auth_sock else {
@@ -1646,6 +1660,7 @@ async fn run_command(
1646
1660
time_sec,
1647
1661
ssh_auth_sock,
1648
1662
key,
1663
+ permslip,
1649
1664
)
1650
1665
. await ?;
1651
1666
}
@@ -1922,8 +1937,9 @@ async fn monorail_unlock(
1922
1937
log : & Logger ,
1923
1938
sp : & SingleSp ,
1924
1939
time_sec : u32 ,
1925
- socket : Option < PathBuf > ,
1940
+ ssh_sock : Option < PathBuf > ,
1926
1941
pub_key : Option < String > ,
1942
+ permslip : bool ,
1927
1943
) -> Result < ( ) > {
1928
1944
let r = sp
1929
1945
. component_action_with_response (
@@ -1946,82 +1962,14 @@ async fn monorail_unlock(
1946
1962
UnlockChallenge :: Trivial { timestamp } => {
1947
1963
UnlockResponse :: Trivial { timestamp }
1948
1964
}
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) ?
1956
1970
} 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" )
2022
1972
}
2023
-
2024
- UnlockResponse :: EcdsaSha2Nistp256 { key, signer_nonce, signature }
2025
1973
}
2026
1974
} ;
2027
1975
sp. component_action (
@@ -2037,6 +1985,129 @@ async fn monorail_unlock(
2037
1985
Ok ( ( ) )
2038
1986
}
2039
1987
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
+
2040
2111
fn ssh_keygen_sign (
2041
2112
socket : PathBuf ,
2042
2113
pub_key : ssh_key:: PublicKey ,
0 commit comments