@@ -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,122 @@ 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 ( "--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
+
2018
2081
fn ssh_keygen_sign (
2019
2082
socket : PathBuf ,
2020
2083
pub_key : ssh_key:: PublicKey ,
0 commit comments