1use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10use crate::actor_id::derive_peer_actor;
11use crate::canonical::canonicalize;
12use crate::crypto::{
13 b64decode, b64encode, chacha20poly1305_decrypt, chacha20poly1305_encrypt, ed25519_verify,
14 hkdf_sha256, x25519_diffie_hellman, x25519_from_bytes, x25519_generate, AeadError, CryptoError,
15 Ed25519Signer, X25519KeyPair,
16};
17use crate::crypto_pq::{ml_dsa_65_sign, ml_dsa_65_verify};
18
19fn is_hybrid_suite(suite: &str) -> bool {
20 suite.ends_with("+ml-dsa-65")
21}
22
23pub const SESSION_VERSION: u32 = 0;
24pub const SESSION_SUITE: &str = "x25519-hkdf-sha256-chacha20poly1305-ed25519";
25pub const SESSION_SUITE_HYBRID_ED25519_MLDSA65: &str =
26 "x25519-hkdf-sha256-chacha20poly1305-ed25519+ml-dsa-65";
27
28pub const KNOWN_SESSION_SUITES: &[&str] = &[SESSION_SUITE, SESSION_SUITE_HYBRID_ED25519_MLDSA65];
30
31#[derive(Debug, thiserror::Error, PartialEq, Eq)]
32pub enum SessionError {
33 #[error("session error: {0}")]
34 Generic(String),
35 #[error("aead failure at seq {0}")]
36 Aead(u64),
37 #[error("crypto error: {0}")]
38 Crypto(String),
39}
40
41impl From<CryptoError> for SessionError {
42 fn from(e: CryptoError) -> Self {
43 SessionError::Crypto(e.to_string())
44 }
45}
46
47#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
48#[serde(tag = "kind", rename = "hello-i")]
49pub struct HelloI {
50 pub version: u32,
51 pub suite: String,
52 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub supported_suites: Option<Vec<String>>,
57 pub session_id: String,
58 pub peer_hint: String,
59 #[serde(skip_serializing_if = "Option::is_none", default)]
61 pub self_hint: Option<String>,
62 pub eph_pub: String,
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(tag = "kind", rename = "hello-r")]
67pub struct HelloR {
68 pub eph_pub: String,
69 pub ident_pub: String,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
74 pub selected_suite: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none", default)]
77 pub self_hint: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none", default)]
82 pub signature_mldsa: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none", default)]
86 pub ident_pub_mldsa: Option<String>,
87 pub signature: String,
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(tag = "kind", rename = "auth")]
92pub struct Auth {
93 pub ident_pub: String,
94 #[serde(skip_serializing_if = "Option::is_none", default)]
97 pub signature_mldsa: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none", default)]
99 pub ident_pub_mldsa: Option<String>,
100 pub signature: String,
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
104#[serde(tag = "kind", rename_all = "kebab-case")]
105pub enum SessionFrame {
106 Data {
107 payload: serde_json::Value,
108 },
109 RekeyReq {
110 eph_pub: String,
111 },
112 RekeyAck {
113 eph_pub: String,
114 },
115 Close {
116 #[serde(skip_serializing_if = "Option::is_none")]
117 reason: Option<String>,
118 },
119 Ping {
120 nonce: String,
121 },
122 Pong {
123 nonce: String,
124 },
125}
126
127#[derive(Clone, Debug, Default)]
128pub struct SessionConfig {
129 pub self_actor: String,
130 pub peer_hint: Option<String>,
131 pub self_hint: Option<String>,
133 pub identity_priv: [u8; 32],
134 pub identity_pub: [u8; 32],
135 pub preferred_suite: Option<String>,
137 pub supported_suites: Option<Vec<String>>,
139 pub identity_mldsa_priv: Option<Vec<u8>>,
141 pub identity_mldsa_pub: Option<Vec<u8>>,
143 pub eph_seed: Option<[u8; 32]>,
144 pub session_id_seed: Option<[u8; 16]>,
145}
146
147pub struct Initiator {
148 cfg: SessionConfig,
149 state: InitiatorState,
150}
151
152enum InitiatorState {
153 Fresh,
154 AwaitingHelloR { hello_i: HelloI, eph_priv: [u8; 32] },
155 Established(SessionState),
156}
157
158impl Initiator {
159 pub fn new(cfg: SessionConfig) -> Self {
160 Initiator {
161 cfg,
162 state: InitiatorState::Fresh,
163 }
164 }
165
166 pub fn established_session(&self) -> Option<&SessionState> {
170 match &self.state {
171 InitiatorState::Established(s) => Some(s),
172 _ => None,
173 }
174 }
175
176 pub fn start(&mut self) -> Result<HelloI, SessionError> {
177 let InitiatorState::Fresh = self.state else {
178 return Err(SessionError::Generic("initiator already started".into()));
179 };
180 let eph = make_ephemeral(&self.cfg.eph_seed);
181 let session_id_bytes = match &self.cfg.session_id_seed {
182 Some(seed) => *seed,
183 None => {
184 let mut buf = [0u8; 16];
185 use rand::RngCore;
186 rand::thread_rng().fill_bytes(&mut buf);
187 buf
188 }
189 };
190 let preferred = self
191 .cfg
192 .preferred_suite
193 .clone()
194 .unwrap_or_else(|| SESSION_SUITE.to_owned());
195 let mut supported = self
196 .cfg
197 .supported_suites
198 .clone()
199 .unwrap_or_else(|| KNOWN_SESSION_SUITES.iter().map(|s| s.to_string()).collect());
200 supported.retain(|s| s != &preferred);
203 supported.insert(0, preferred.clone());
204 let hello_i = HelloI {
205 version: SESSION_VERSION,
206 suite: preferred,
207 supported_suites: Some(supported),
208 session_id: b64encode(&session_id_bytes),
209 peer_hint: self.cfg.peer_hint.clone().unwrap_or_default(),
210 self_hint: self.cfg.self_hint.clone(),
211 eph_pub: b64encode(&eph.public),
212 };
213 self.state = InitiatorState::AwaitingHelloR {
214 hello_i: hello_i.clone(),
215 eph_priv: eph.private,
216 };
217 Ok(hello_i)
218 }
219
220 pub fn process_hello_r(&mut self, msg: HelloR) -> Result<(Auth, SessionState), SessionError> {
221 let (hello_i, eph_priv) = match std::mem::replace(&mut self.state, InitiatorState::Fresh) {
222 InitiatorState::AwaitingHelloR { hello_i, eph_priv } => (hello_i, eph_priv),
223 _ => return Err(SessionError::Generic("not awaiting hello-r".into())),
224 };
225
226 let hello_r_unsigned = HelloR {
230 signature: String::new(),
231 signature_mldsa: None,
232 ..msg.clone()
233 };
234 let transcript = canonical_concat(&[
235 &serde_json::to_value(&hello_i).map_err(|e| SessionError::Generic(e.to_string()))?,
236 &serde_json::to_value(&hello_r_unsigned)
237 .map_err(|e| SessionError::Generic(e.to_string()))?,
238 ])?;
239 let transcript_hash = Sha256::digest(transcript.as_bytes());
240
241 let ident_pub = b64decode(&msg.ident_pub)?;
242 let sig = b64decode(&msg.signature)?;
243 ed25519_verify(&ident_pub, &transcript_hash, &sig)
244 .map_err(|_| SessionError::Generic("responder identity signature invalid".into()))?;
245
246 let negotiated_suite = msg
247 .selected_suite
248 .clone()
249 .unwrap_or_else(|| hello_i.suite.clone());
250 if is_hybrid_suite(&negotiated_suite) {
251 let pq_sig_b64 = msg.signature_mldsa.as_deref().ok_or_else(|| {
252 SessionError::Generic(format!(
253 "negotiated hybrid suite {} but HelloR missing signature_mldsa",
254 negotiated_suite
255 ))
256 })?;
257 let pq_pub_b64 = msg.ident_pub_mldsa.as_deref().ok_or_else(|| {
258 SessionError::Generic(format!(
259 "negotiated hybrid suite {} but HelloR missing ident_pub_mldsa",
260 negotiated_suite
261 ))
262 })?;
263 let pq_sig = b64decode(pq_sig_b64)?;
264 let pq_pub = b64decode(pq_pub_b64)?;
265 if !ml_dsa_65_verify(&pq_pub, transcript_hash.as_slice(), &pq_sig) {
266 return Err(SessionError::Generic(
267 "responder ml-dsa-65 signature invalid".into(),
268 ));
269 }
270 }
271
272 let peer_eph: [u8; 32] = b64decode(&msg.eph_pub)?
273 .try_into()
274 .map_err(|_| SessionError::Generic("eph_pub not 32 bytes".into()))?;
275 let shared = x25519_diffie_hellman(&eph_priv, &peer_eph);
276
277 let auth_unsigned = Auth {
278 ident_pub: b64encode(&self.cfg.identity_pub),
279 signature_mldsa: None,
280 ident_pub_mldsa: if is_hybrid_suite(&negotiated_suite) {
281 self.cfg.identity_mldsa_pub.as_deref().map(b64encode)
282 } else {
283 None
284 },
285 signature: String::new(),
286 };
287 let full_transcript = canonical_concat(&[
288 &serde_json::to_value(&hello_i).map_err(|e| SessionError::Generic(e.to_string()))?,
289 &serde_json::to_value(&msg).map_err(|e| SessionError::Generic(e.to_string()))?,
290 &serde_json::to_value(&auth_unsigned)
291 .map_err(|e| SessionError::Generic(e.to_string()))?,
292 ])?;
293 let full_hash = Sha256::digest(full_transcript.as_bytes());
294
295 let signer = Ed25519Signer::from_bytes(&self.cfg.identity_priv);
296 let auth_sig = signer.sign(&full_hash);
297 let auth_pq_sig = if is_hybrid_suite(&negotiated_suite) {
298 let priv_bytes = self.cfg.identity_mldsa_priv.as_ref().ok_or_else(|| {
299 SessionError::Generic(format!(
300 "negotiated hybrid suite {} but initiator is missing identity_mldsa_priv",
301 negotiated_suite
302 ))
303 })?;
304 Some(b64encode(
305 &ml_dsa_65_sign(priv_bytes, full_hash.as_slice())
306 .map_err(|e| SessionError::Generic(e.to_string()))?,
307 ))
308 } else {
309 None
310 };
311 let auth = Auth {
312 ident_pub: b64encode(&self.cfg.identity_pub),
313 signature_mldsa: auth_pq_sig,
314 ident_pub_mldsa: auth_unsigned.ident_pub_mldsa.clone(),
315 signature: b64encode(&auth_sig),
316 };
317
318 let session_id_bytes = b64decode(&hello_i.session_id)?;
319 let peer_actor = derive_peer_actor(&ident_pub)
320 .map_err(|e| SessionError::Generic(format!("derive_peer_actor: {e}")))?;
321 let peer_claim = msg.self_hint.clone().filter(|s| !s.is_empty());
323 let session = SessionState::derive_with_claim(
324 Role::Initiator,
325 &shared,
326 &session_id_bytes,
327 &full_hash,
328 &self.cfg.self_actor,
329 &peer_actor,
330 peer_claim,
331 );
332 self.state = InitiatorState::Established(session.clone());
333 Ok((auth, session))
334 }
335}
336
337pub struct Responder {
338 cfg: SessionConfig,
339 state: ResponderState,
340}
341
342enum ResponderState {
343 Fresh,
344 AwaitingAuth {
345 hello_i: HelloI,
346 hello_r: HelloR,
347 shared: [u8; 32],
348 },
349 Established(SessionState),
350}
351
352impl Responder {
353 pub fn established_session(&self) -> Option<&SessionState> {
356 match &self.state {
357 ResponderState::Established(s) => Some(s),
358 _ => None,
359 }
360 }
361
362 pub fn new(cfg: SessionConfig) -> Self {
363 Responder {
364 cfg,
365 state: ResponderState::Fresh,
366 }
367 }
368
369 pub fn process_hello_i(&mut self, msg: HelloI) -> Result<HelloR, SessionError> {
370 let ResponderState::Fresh = self.state else {
371 return Err(SessionError::Generic("responder already engaged".into()));
372 };
373 if msg.version != SESSION_VERSION {
374 return Err(SessionError::Generic(format!(
375 "unsupported version {}",
376 msg.version
377 )));
378 }
379 let our_supported: Vec<String> = self
383 .cfg
384 .supported_suites
385 .clone()
386 .unwrap_or_else(|| KNOWN_SESSION_SUITES.iter().map(|s| s.to_string()).collect());
387 let chosen = match &msg.supported_suites {
388 Some(client_supports) => client_supports
389 .iter()
390 .find(|s| our_supported.iter().any(|o| o == *s))
391 .cloned()
392 .ok_or_else(|| {
393 SessionError::Generic(format!(
394 "no mutually-supported suite (peer offered {:?}, we support {:?})",
395 client_supports, our_supported
396 ))
397 })?,
398 None => {
399 if !our_supported.iter().any(|s| s == &msg.suite) {
400 return Err(SessionError::Generic(format!(
401 "unsupported suite {}",
402 msg.suite
403 )));
404 }
405 msg.suite.clone()
406 }
407 };
408
409 let eph = make_ephemeral(&self.cfg.eph_seed);
410 let peer_eph: [u8; 32] = b64decode(&msg.eph_pub)?
411 .try_into()
412 .map_err(|_| SessionError::Generic("eph_pub not 32 bytes".into()))?;
413 let shared = x25519_diffie_hellman(&eph.private, &peer_eph);
414
415 let hello_r_unsigned = HelloR {
416 eph_pub: b64encode(&eph.public),
417 ident_pub: b64encode(&self.cfg.identity_pub),
418 selected_suite: Some(chosen.clone()),
419 self_hint: self.cfg.self_hint.clone(),
420 signature_mldsa: None,
421 ident_pub_mldsa: if is_hybrid_suite(&chosen) {
422 self.cfg.identity_mldsa_pub.as_deref().map(b64encode)
423 } else {
424 None
425 },
426 signature: String::new(),
427 };
428 let transcript = canonical_concat(&[
429 &serde_json::to_value(&msg).map_err(|e| SessionError::Generic(e.to_string()))?,
430 &serde_json::to_value(&hello_r_unsigned)
431 .map_err(|e| SessionError::Generic(e.to_string()))?,
432 ])?;
433 let transcript_hash = Sha256::digest(transcript.as_bytes());
434
435 let signer = Ed25519Signer::from_bytes(&self.cfg.identity_priv);
436 let sig = signer.sign(&transcript_hash);
437 let pq_sig = if is_hybrid_suite(&chosen) {
438 let priv_bytes = self.cfg.identity_mldsa_priv.as_ref().ok_or_else(|| {
439 SessionError::Generic(format!(
440 "negotiated hybrid suite {} but responder is missing identity_mldsa_priv",
441 chosen
442 ))
443 })?;
444 Some(b64encode(
445 &ml_dsa_65_sign(priv_bytes, transcript_hash.as_slice())
446 .map_err(|e| SessionError::Generic(e.to_string()))?,
447 ))
448 } else {
449 None
450 };
451 let hello_r = HelloR {
452 signature: b64encode(&sig),
453 signature_mldsa: pq_sig,
454 ..hello_r_unsigned
455 };
456
457 self.state = ResponderState::AwaitingAuth {
458 hello_i: msg,
459 hello_r: hello_r.clone(),
460 shared,
461 };
462 Ok(hello_r)
463 }
464
465 pub fn process_auth(&mut self, msg: Auth) -> Result<SessionState, SessionError> {
466 let (hello_i, hello_r, shared) =
467 match std::mem::replace(&mut self.state, ResponderState::Fresh) {
468 ResponderState::AwaitingAuth {
469 hello_i,
470 hello_r,
471 shared,
472 } => (hello_i, hello_r, shared),
473 _ => return Err(SessionError::Generic("not awaiting auth".into())),
474 };
475
476 let auth_unsigned = Auth {
477 signature: String::new(),
478 signature_mldsa: None,
479 ..msg.clone()
480 };
481 let full_transcript = canonical_concat(&[
482 &serde_json::to_value(&hello_i).map_err(|e| SessionError::Generic(e.to_string()))?,
483 &serde_json::to_value(&hello_r).map_err(|e| SessionError::Generic(e.to_string()))?,
484 &serde_json::to_value(&auth_unsigned)
485 .map_err(|e| SessionError::Generic(e.to_string()))?,
486 ])?;
487 let full_hash = Sha256::digest(full_transcript.as_bytes());
488
489 let ident_pub = b64decode(&msg.ident_pub)?;
490 let sig = b64decode(&msg.signature)?;
491 ed25519_verify(&ident_pub, &full_hash, &sig)
492 .map_err(|_| SessionError::Generic("initiator identity signature invalid".into()))?;
493
494 let negotiated_suite = hello_r
495 .selected_suite
496 .clone()
497 .unwrap_or_else(|| hello_i.suite.clone());
498 if is_hybrid_suite(&negotiated_suite) {
499 let pq_sig_b64 = msg.signature_mldsa.as_deref().ok_or_else(|| {
500 SessionError::Generic(format!(
501 "negotiated hybrid suite {} but Auth missing signature_mldsa",
502 negotiated_suite
503 ))
504 })?;
505 let pq_pub_b64 = msg.ident_pub_mldsa.as_deref().ok_or_else(|| {
506 SessionError::Generic(format!(
507 "negotiated hybrid suite {} but Auth missing ident_pub_mldsa",
508 negotiated_suite
509 ))
510 })?;
511 let pq_sig = b64decode(pq_sig_b64)?;
512 let pq_pub = b64decode(pq_pub_b64)?;
513 if !ml_dsa_65_verify(&pq_pub, full_hash.as_slice(), &pq_sig) {
514 return Err(SessionError::Generic(
515 "initiator ml-dsa-65 signature invalid".into(),
516 ));
517 }
518 }
519
520 let session_id_bytes = b64decode(&hello_i.session_id)?;
521 let peer_actor = derive_peer_actor(&ident_pub)
522 .map_err(|e| SessionError::Generic(format!("derive_peer_actor: {e}")))?;
523 let peer_claim = hello_i.self_hint.clone().filter(|s| !s.is_empty());
525 let session = SessionState::derive_with_claim(
526 Role::Responder,
527 &shared,
528 &session_id_bytes,
529 &full_hash,
530 &self.cfg.self_actor,
531 &peer_actor,
532 peer_claim,
533 );
534 self.state = ResponderState::Established(session.clone());
535 Ok(session)
536 }
537}
538
539#[derive(Clone, Debug, PartialEq, Eq)]
540pub enum Role {
541 Initiator,
542 Responder,
543}
544
545#[derive(Clone, Debug)]
546pub struct SessionState {
547 pub self_actor: String,
548 pub peer_actor: String,
550 pub peer_actor_claim: Option<String>,
552 pub session_id: Vec<u8>,
553 pub generation: u32,
554 pub send_key: [u8; 32],
555 pub recv_key: [u8; 32],
556 pub send_seq: u64,
557 pub recv_seq: u64,
558 pub closed: bool,
559 pending_rekey_priv: Option<[u8; 32]>,
560}
561
562impl SessionState {
563 pub fn derive(
564 role: Role,
565 shared_secret: &[u8; 32],
566 session_id: &[u8],
567 transcript_hash: &[u8],
568 self_actor: &str,
569 peer_actor: &str,
570 ) -> Self {
571 Self::derive_with_claim(
572 role,
573 shared_secret,
574 session_id,
575 transcript_hash,
576 self_actor,
577 peer_actor,
578 None,
579 )
580 }
581
582 pub fn derive_with_claim(
583 role: Role,
584 shared_secret: &[u8; 32],
585 session_id: &[u8],
586 transcript_hash: &[u8],
587 self_actor: &str,
588 peer_actor: &str,
589 peer_actor_claim: Option<String>,
590 ) -> Self {
591 let mut info = b"tf-session/v0/keys".to_vec();
592 info.extend_from_slice(transcript_hash);
593 let ikm = hkdf_sha256(shared_secret, session_id, &info, 64);
594 let i_to_r: [u8; 32] = ikm[0..32].try_into().unwrap();
595 let r_to_i: [u8; 32] = ikm[32..64].try_into().unwrap();
596 let (send_key, recv_key) = match role {
597 Role::Initiator => (i_to_r, r_to_i),
598 Role::Responder => (r_to_i, i_to_r),
599 };
600 SessionState {
601 self_actor: self_actor.to_owned(),
602 peer_actor: peer_actor.to_owned(),
603 peer_actor_claim,
604 session_id: session_id.to_vec(),
605 generation: 0,
606 send_key,
607 recv_key,
608 send_seq: 0,
609 recv_seq: 0,
610 closed: false,
611 pending_rekey_priv: None,
612 }
613 }
614
615 pub fn encrypt(&mut self, frame: &SessionFrame) -> Result<Vec<u8>, SessionError> {
616 if self.closed {
617 return Err(SessionError::Generic("session is closed".into()));
618 }
619 let body_value =
620 serde_json::to_value(frame).map_err(|e| SessionError::Generic(e.to_string()))?;
621 let body = canonicalize(&body_value).map_err(|e| SessionError::Generic(e.to_string()))?;
622 let plaintext = body.into_bytes();
623 let seq = self.send_seq;
624 let nonce = nonce_for(seq);
625 let length = 8 + plaintext.len() + 16;
626 if length > u32::MAX as usize {
627 return Err(SessionError::Generic("frame too long".into()));
628 }
629 let aad = make_aad(length as u32, seq);
630 let ct = chacha20poly1305_encrypt(&self.send_key, &nonce, &aad, &plaintext);
631 let mut out = Vec::with_capacity(4 + length);
632 out.extend_from_slice(&(length as u32).to_be_bytes());
633 out.extend_from_slice(&seq.to_be_bytes());
634 out.extend_from_slice(&ct);
635 self.send_seq = seq.wrapping_add(1);
636 Ok(out)
637 }
638
639 pub fn decrypt(&mut self, bytes: &[u8]) -> Result<SessionFrame, SessionError> {
640 if self.closed {
641 return Err(SessionError::Generic("session is closed".into()));
642 }
643 if bytes.len() < 12 + 16 {
644 return Err(SessionError::Generic("frame too short".into()));
645 }
646 let length = u32::from_be_bytes(bytes[0..4].try_into().unwrap()) as usize;
647 if 4 + length != bytes.len() {
648 return Err(SessionError::Generic("length mismatch".into()));
649 }
650 let seq = u64::from_be_bytes(bytes[4..12].try_into().unwrap());
651 if seq != self.recv_seq {
652 return Err(SessionError::Generic(format!(
653 "out-of-order frame: got {}, expected {}",
654 seq, self.recv_seq
655 )));
656 }
657 let aad = make_aad(length as u32, seq);
658 let nonce = nonce_for(seq);
659 let pt = chacha20poly1305_decrypt(&self.recv_key, &nonce, &aad, &bytes[12..])
660 .map_err(|_: AeadError| SessionError::Aead(seq))?;
661 let value: serde_json::Value =
662 serde_json::from_slice(&pt).map_err(|e| SessionError::Generic(e.to_string()))?;
663 let frame: SessionFrame =
664 serde_json::from_value(value).map_err(|e| SessionError::Generic(e.to_string()))?;
665 self.recv_seq = seq.wrapping_add(1);
666 Ok(frame)
667 }
668
669 pub fn request_rekey(&mut self, seed: Option<[u8; 32]>) -> Result<Vec<u8>, SessionError> {
670 let eph = make_ephemeral(&seed);
671 self.pending_rekey_priv = Some(eph.private);
672 self.encrypt(&SessionFrame::RekeyReq {
673 eph_pub: b64encode(&eph.public),
674 })
675 }
676
677 pub fn process_rekey_req(
678 &mut self,
679 peer_eph_pub_b64: &str,
680 seed: Option<[u8; 32]>,
681 ) -> Result<Vec<u8>, SessionError> {
682 let eph = make_ephemeral(&seed);
683 let peer_eph: [u8; 32] = b64decode(peer_eph_pub_b64)?
684 .try_into()
685 .map_err(|_| SessionError::Generic("eph_pub not 32 bytes".into()))?;
686 let shared = x25519_diffie_hellman(&eph.private, &peer_eph);
687 let ack = self.encrypt(&SessionFrame::RekeyAck {
688 eph_pub: b64encode(&eph.public),
689 })?;
690 self.rotate_keys(&shared);
691 Ok(ack)
692 }
693
694 pub fn process_rekey_ack(&mut self, peer_eph_pub_b64: &str) -> Result<(), SessionError> {
695 let pending = self
696 .pending_rekey_priv
697 .take()
698 .ok_or_else(|| SessionError::Generic("no pending rekey".into()))?;
699 let peer_eph: [u8; 32] = b64decode(peer_eph_pub_b64)?
700 .try_into()
701 .map_err(|_| SessionError::Generic("eph_pub not 32 bytes".into()))?;
702 let shared = x25519_diffie_hellman(&pending, &peer_eph);
703 self.rotate_keys(&shared);
704 Ok(())
705 }
706
707 fn rotate_keys(&mut self, shared: &[u8; 32]) {
708 let send_hex = hex_lower_32(&self.send_key);
710 let recv_hex = hex_lower_32(&self.recv_key);
711 let send_is_lower = send_hex < recv_hex;
712 let lo = if send_is_lower {
713 &self.send_key
714 } else {
715 &self.recv_key
716 };
717 let hi = if send_is_lower {
718 &self.recv_key
719 } else {
720 &self.send_key
721 };
722 let mut concat = Vec::with_capacity(64);
723 concat.extend_from_slice(lo);
724 concat.extend_from_slice(hi);
725 let prev_hash = Sha256::digest(&concat);
726
727 let info_label = format!("tf-session/v0/keys/g{}", self.generation + 1);
728 let mut info = info_label.into_bytes();
729 info.extend_from_slice(&prev_hash);
730 let ikm = hkdf_sha256(shared, &self.session_id, &info, 64);
731 let k1: [u8; 32] = ikm[0..32].try_into().unwrap();
732 let k2: [u8; 32] = ikm[32..64].try_into().unwrap();
733 if send_is_lower {
734 self.send_key = k1;
735 self.recv_key = k2;
736 } else {
737 self.send_key = k2;
738 self.recv_key = k1;
739 }
740 self.send_seq = 0;
741 self.recv_seq = 0;
742 self.generation += 1;
743 }
744}
745
746fn nonce_for(seq: u64) -> [u8; 12] {
747 let mut out = [0u8; 12];
748 out[4..].copy_from_slice(&seq.to_be_bytes());
749 out
750}
751
752fn make_aad(length: u32, seq: u64) -> [u8; 12] {
753 let mut out = [0u8; 12];
754 out[0..4].copy_from_slice(&length.to_be_bytes());
755 out[4..].copy_from_slice(&seq.to_be_bytes());
756 out
757}
758
759fn make_ephemeral(seed: &Option<[u8; 32]>) -> X25519KeyPair {
760 match seed {
761 Some(s) => x25519_from_bytes(s),
762 None => {
763 let mut rng = rand::thread_rng();
764 x25519_generate(&mut rng)
765 }
766 }
767}
768
769fn canonical_concat(values: &[&serde_json::Value]) -> Result<String, SessionError> {
770 let mut out = String::new();
771 for v in values {
772 let s = canonicalize(v).map_err(|e| SessionError::Generic(e.to_string()))?;
773 out.push_str(&s);
774 }
775 Ok(out)
776}
777
778fn hex_lower_32(bytes: &[u8; 32]) -> String {
779 let mut s = String::with_capacity(64);
780 for b in bytes {
781 s.push_str(&format!("{:02x}", b));
782 }
783 s
784}