@@ -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,28 @@ 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
+ requires = "key"
579
+ ) ]
580
+ permslip : bool ,
569
581
} ,
570
582
571
583
/// Lock the technician port
@@ -1605,6 +1617,7 @@ async fn run_command(
1605
1617
cmd : UnlockGroup { time, list } ,
1606
1618
key,
1607
1619
ssh_auth_sock,
1620
+ permslip,
1608
1621
} => {
1609
1622
if list {
1610
1623
let Some ( ssh_auth_sock) = ssh_auth_sock else {
@@ -1624,6 +1637,7 @@ async fn run_command(
1624
1637
time_sec,
1625
1638
ssh_auth_sock,
1626
1639
key,
1640
+ permslip,
1627
1641
)
1628
1642
. await ?;
1629
1643
}
@@ -1900,8 +1914,9 @@ async fn monorail_unlock(
1900
1914
log : & Logger ,
1901
1915
sp : & SingleSp ,
1902
1916
time_sec : u32 ,
1903
- socket : Option < PathBuf > ,
1917
+ ssh_sock : Option < PathBuf > ,
1904
1918
pub_key : Option < String > ,
1919
+ permslip : bool ,
1905
1920
) -> Result < ( ) > {
1906
1921
let r = sp
1907
1922
. component_action_with_response (
@@ -1924,82 +1939,14 @@ async fn monorail_unlock(
1924
1939
UnlockChallenge :: Trivial { timestamp } => {
1925
1940
UnlockResponse :: Trivial { timestamp }
1926
1941
}
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) ?
1934
1947
} 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" )
2000
1949
}
2001
-
2002
- UnlockResponse :: EcdsaSha2Nistp256 { key, signer_nonce, signature }
2003
1950
}
2004
1951
} ;
2005
1952
sp. component_action (
@@ -2015,6 +1962,123 @@ async fn monorail_unlock(
2015
1962
Ok ( ( ) )
2016
1963
}
2017
1964
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
+
2018
2082
fn ssh_keygen_sign (
2019
2083
socket : PathBuf ,
2020
2084
pub_key : ssh_key:: PublicKey ,
0 commit comments