22
33use anyhow:: { Context , Result } ;
44use auths_core:: config:: EnvironmentConfig ;
5+ use auths_core:: pairing:: types:: Base64UrlEncoded ;
6+ use auths_core:: pairing:: { PairingResponse , PairingToken } ;
7+ use auths_core:: ports:: pairing:: PairingRelayClient ;
58use auths_infra_http:: HttpPairingRelayClient ;
6- use auths_sdk:: pairing:: {
7- PairingCompletionResult , join_pairing_session, load_device_signing_material,
8- } ;
9+ use auths_pairing_protocol:: sas;
10+ use auths_sdk:: pairing:: { load_device_signing_material, validate_short_code} ;
911use chrono:: Utc ;
1012use console:: style;
1113
@@ -20,8 +22,7 @@ pub(crate) async fn handle_join(
2022 registry : & str ,
2123 env_config : & EnvironmentConfig ,
2224) -> Result < ( ) > {
23- let normalized =
24- auths_sdk:: pairing:: validate_short_code ( code) . map_err ( |e| anyhow:: anyhow!( "{}" , e) ) ?;
25+ let normalized = validate_short_code ( code) . map_err ( |e| anyhow:: anyhow!( "{}" , e) ) ?;
2526
2627 let formatted = format ! ( "{}-{}" , & normalized[ ..3 ] , & normalized[ 3 ..] ) ;
2728
@@ -67,42 +68,148 @@ pub(crate) async fn handle_join(
6768 ) ;
6869 println ! ( ) ;
6970
70- let create_spinner = create_wait_spinner ( & format ! ( "{GEAR}Creating and submitting response..." ) ) ;
71+ // Look up the session by short code
72+ let session_data = relay
73+ . lookup_by_code ( registry, & normalized)
74+ . await
75+ . map_err ( |e| anyhow:: anyhow!( "Failed to look up session: {}" , e) ) ?;
76+
77+ let token_data = session_data
78+ . token
79+ . ok_or_else ( || anyhow:: anyhow!( "session has no token data" ) ) ?;
80+
81+ let token = PairingToken {
82+ controller_did : token_data. controller_did . clone ( ) ,
83+ endpoint : registry. to_string ( ) ,
84+ short_code : normalized. clone ( ) ,
85+ ephemeral_pubkey : token_data. ephemeral_pubkey . to_string ( ) ,
86+ expires_at : chrono:: DateTime :: from_timestamp ( token_data. expires_at , 0 )
87+ . unwrap_or_else ( Utc :: now) ,
88+ capabilities : token_data. capabilities . clone ( ) ,
89+ } ;
90+
91+ if token. is_expired ( Utc :: now ( ) ) {
92+ anyhow:: bail!( "Session expired" ) ;
93+ }
94+
95+ let create_spinner = create_wait_spinner ( & format ! ( "{GEAR}Creating pairing response..." ) ) ;
7196
72- match join_pairing_session (
73- code,
74- registry,
75- & relay,
97+ // Create the response + ECDH
98+ let ( pairing_response, shared_secret) = PairingResponse :: create (
7699 Utc :: now ( ) ,
77- & material,
100+ & token,
101+ & material. seed ,
102+ & material. public_key ,
103+ material. device_did . to_string ( ) ,
78104 Some ( hostname ( ) ) ,
79105 )
80- . await
81- . map_err ( |e| anyhow:: anyhow!( "{}" , e) ) ?
82- {
83- PairingCompletionResult :: Success { .. } => {
84- create_spinner. finish_with_message ( format ! ( "{CHECK}Response submitted" ) ) ;
85- }
86- PairingCompletionResult :: Fallback { error, .. } => {
87- create_spinner. finish_and_clear ( ) ;
88- anyhow:: bail!( "Failed to submit pairing response: {}" , error) ;
89- }
106+ . map_err ( |e| anyhow:: anyhow!( "Failed to create pairing response: {}" , e) ) ?;
107+
108+ // Derive SAS from shared secret with transcript binding
109+ let initiator_x25519_pub = token
110+ . ephemeral_pubkey_bytes ( )
111+ . map_err ( |e| anyhow:: anyhow!( "Invalid initiator pubkey: {}" , e) ) ?;
112+ let responder_x25519_pub = pairing_response
113+ . device_x25519_pubkey_bytes ( )
114+ . map_err ( |e| anyhow:: anyhow!( "Invalid responder pubkey: {}" , e) ) ?;
115+
116+ let sas_bytes = sas:: derive_sas (
117+ & shared_secret,
118+ & initiator_x25519_pub,
119+ & responder_x25519_pub,
120+ & normalized,
121+ ) ;
122+ let transport_key = sas:: derive_transport_key (
123+ & shared_secret,
124+ & initiator_x25519_pub,
125+ & responder_x25519_pub,
126+ & normalized,
127+ ) ;
128+
129+ // Submit the response to the relay
130+ let submit_req = auths_core:: pairing:: types:: SubmitResponseRequest {
131+ device_x25519_pubkey : Base64UrlEncoded :: from_raw (
132+ pairing_response. device_x25519_pubkey . clone ( ) ,
133+ ) ,
134+ device_signing_pubkey : Base64UrlEncoded :: from_raw (
135+ pairing_response. device_signing_pubkey . clone ( ) ,
136+ ) ,
137+ device_did : pairing_response. device_did . clone ( ) ,
138+ signature : Base64UrlEncoded :: from_raw ( pairing_response. signature . clone ( ) ) ,
139+ device_name : pairing_response. device_name . clone ( ) ,
140+ } ;
141+
142+ relay
143+ . submit_response ( registry, & session_data. session_id , & submit_req)
144+ . await
145+ . map_err ( |e| anyhow:: anyhow!( "Failed to submit response: {}" , e) ) ?;
146+
147+ create_spinner. finish_with_message ( format ! ( "{CHECK}Response submitted" ) ) ;
148+
149+ // SAS verification ceremony
150+ let confirmed = prompt_sas_confirmation ( & sas_bytes) ?;
151+ if !confirmed {
152+ display_sas_mismatch_warning ( ) ;
153+ drop ( transport_key) ;
154+ anyhow:: bail!( "SAS verification failed — pairing aborted" ) ;
155+ }
156+
157+ // Wait for encrypted attestation from initiator
158+ let wait_spinner = create_wait_spinner ( & format ! (
159+ "{GEAR}Waiting for initiator to confirm and send attestation..."
160+ ) ) ;
161+
162+ let confirmation = relay
163+ . get_confirmation ( registry, & session_data. session_id )
164+ . await
165+ . map_err ( |e| anyhow:: anyhow!( "Failed to get confirmation: {}" , e) ) ?;
166+
167+ if confirmation. aborted {
168+ wait_spinner. finish_and_clear ( ) ;
169+ println ! ( ) ;
170+ println ! (
171+ " {}{}" ,
172+ WARN ,
173+ style( "The other device rejected the pairing." ) . red( ) . bold( )
174+ ) ;
175+ println ! ( " {}" , style( "No attestation was created." ) . dim( ) ) ;
176+ println ! ( ) ;
177+ drop ( transport_key) ;
178+ anyhow:: bail!( "Initiator rejected SAS — pairing aborted" ) ;
179+ }
180+
181+ if let Some ( encrypted) = confirmation. encrypted_attestation {
182+ let ciphertext = base64:: Engine :: decode (
183+ & base64:: engine:: general_purpose:: URL_SAFE_NO_PAD ,
184+ & encrypted,
185+ )
186+ . context ( "Invalid base64 in encrypted attestation" ) ?;
187+
188+ let _attestation_json = sas:: decrypt_from_transport ( & ciphertext, transport_key. as_bytes ( ) )
189+ . map_err ( |e| anyhow:: anyhow!( "Failed to decrypt attestation: {}" , e) ) ?;
190+
191+ wait_spinner. finish_with_message ( format ! ( "{CHECK}Attestation received and decrypted" ) ) ;
192+
193+ // TODO(fn-43.6): verify and store attestation locally
194+ } else {
195+ wait_spinner. finish_and_clear ( ) ;
196+ println ! ( ) ;
197+ println ! (
198+ " {}{}" ,
199+ WARN ,
200+ style( "No attestation received from initiator." ) . yellow( )
201+ ) ;
202+ println ! ( ) ;
90203 }
91204
92205 println ! ( ) ;
93206 println ! (
94207 "{}" ,
95- style( format!( "━━━ {CHECK}Response Submitted ━━━" ) )
208+ style( format!( "━━━ {CHECK}Pairing Complete ━━━" ) )
96209 . green( )
97210 . bold( )
98211 ) ;
99212 println ! ( ) ;
100- println ! (
101- " {}" ,
102- style( "The initiating device will verify the response and create" ) . dim( )
103- ) ;
104- println ! ( " {}" , style( "a device attestation for this device." ) . dim( ) ) ;
105- println ! ( ) ;
106213
107214 Ok ( ( ) )
108215}
0 commit comments