1use std::fmt;
29use std::sync::Arc;
30
31use sha2::{Digest, Sha384};
32
33use crate::binding::{
34 verify_connect_binding, verify_status, verify_tls_binding, BindingError, SignedTbs,
35};
36use crate::cbor::{self, Value};
37use crate::node_key::{
38 carried_key_well_formed, node_id_of, puzzle_solved, signature_size, verify, KeyError, NodeKey,
39};
40use crate::profile::Profile;
41
42#[cfg(test)]
43mod v5_tests;
44
45pub const VERSION: i64 = 4;
48
49pub const VERSION_5: i64 = 5;
52
53pub const EXPORTER_LABEL: &str = "EXPORTER-macula-session-v1";
56
57const NONCE_SIZE: usize = 32;
58const EXPORTER_SIZE: usize = 32;
59const MAX_PROTOCOL_INT: i64 = 1 << 53;
60const MLDSA_KEY_SIZE: usize = 2592;
61const CONNECT_PROOF_LABEL: &[u8] = b"MACULA-PQ-CONNECT-PROOF-V1";
62const CONNECT_PROOF_LABEL_V2: &[u8] = b"MACULA-PQ-CONNECT-PROOF-V2";
63const SESSION_PROOF_LABEL: &[u8] = b"MACULA-PQ-SESSION-PROOF-V1";
64
65pub type Exporter = dyn Fn(&str, &[u8], usize) -> Option<Vec<u8>> + Send + Sync;
69
70const OPENER_KEYS: &[&str] = &["frame_type", "version"];
71const CHALLENGE_KEYS: &[&str] = &[
72 "frame_type",
73 "identity_key",
74 "nonce",
75 "profile",
76 "tls_binding",
77 "tls_status",
78 "version",
79];
80const CONNECT_KEYS: &[&str] = &[
84 "capabilities",
85 "connect_binding",
86 "connect_key",
87 "connect_status",
88 "frame_type",
89 "identity_key",
90 "member_endorsement",
91 "proof",
92 "version",
93];
94const HELLO_ACCEPTED_KEYS: &[&str] = &["accepted", "capabilities", "frame_type", "version"];
95const HELLO_REFUSED_KEYS: &[&str] = &[
96 "accepted",
97 "capabilities",
98 "frame_type",
99 "refusal_code",
100 "version",
101];
102const HELLO_PROVED_KEYS: &[&str] = &[
103 "accepted",
104 "capabilities",
105 "frame_type",
106 "session_proof",
107 "version",
108];
109const STATUS_KEYS: &[&str] = &["frame_type", "statement", "version"];
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum RefusalCode {
114 UnsupportedVersion,
116 PuzzleInvalid,
118 SessionProofRate,
121 NotAccepted,
123}
124
125impl RefusalCode {
126 fn name(self) -> &'static str {
127 match self {
128 RefusalCode::UnsupportedVersion => "unsupported_version",
129 RefusalCode::PuzzleInvalid => "puzzle_invalid",
130 RefusalCode::SessionProofRate => "session_proof_rate",
131 RefusalCode::NotAccepted => "not_accepted",
132 }
133 }
134
135 fn parse(name: &str) -> Option<RefusalCode> {
136 match name {
137 "unsupported_version" => Some(RefusalCode::UnsupportedVersion),
138 "puzzle_invalid" => Some(RefusalCode::PuzzleInvalid),
139 "session_proof_rate" => Some(RefusalCode::SessionProofRate),
140 "not_accepted" => Some(RefusalCode::NotAccepted),
141 _ => None,
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum HandshakeError {
150 UnexpectedFrame,
152 UnsupportedVersion,
154 Malformed,
157 ProfileMismatch,
159 KeyPurposeReuse,
162 PeerIdentityMismatch {
164 expected: [u8; 32],
165 derived: [u8; 32],
166 },
167 PuzzleInvalid,
169 ProofInvalid,
171 Refused(RefusalCode),
173 InvalidStationSession,
175 SessionProofInvalid,
178 SessionProofMissing,
180 SessionProofRate,
182 V4HelloToV5Connect,
184 ExporterUnavailable,
186 Binding(BindingError),
188 Key(KeyError),
190}
191
192impl fmt::Display for HandshakeError {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 HandshakeError::UnexpectedFrame => f.write_str("unexpected frame"),
196 HandshakeError::UnsupportedVersion => f.write_str("unsupported frame version"),
197 HandshakeError::Malformed => f.write_str("malformed frame"),
198 HandshakeError::ProfileMismatch => f.write_str("the peer names another profile"),
199 HandshakeError::KeyPurposeReuse => f.write_str("a key would serve two purposes"),
200 HandshakeError::PeerIdentityMismatch { expected, derived } => write!(
201 f,
202 "dialed node_id {}, but the station's key derives {}",
203 hex_of(expected),
204 hex_of(derived)
205 ),
206 HandshakeError::PuzzleInvalid => f.write_str("the node_id does not meet the puzzle"),
207 HandshakeError::ProofInvalid => f.write_str("the CONNECT proof does not verify"),
208 HandshakeError::Refused(code) => {
209 write!(f, "the station refused the connection: {}", code.name())
210 }
211 HandshakeError::InvalidStationSession => {
212 f.write_str("the station session has an unknown puzzle difficulty")
213 }
214 HandshakeError::SessionProofInvalid => f.write_str("the session proof does not verify"),
215 HandshakeError::SessionProofMissing => {
216 f.write_str("the HELLO carries no session proof")
217 }
218 HandshakeError::SessionProofRate => {
219 f.write_str("the station's session proof budget is spent")
220 }
221 HandshakeError::V4HelloToV5Connect => f.write_str("a v4 HELLO accepted a v5 CONNECT"),
222 HandshakeError::ExporterUnavailable => f.write_str("the TLS exporter is unavailable"),
223 HandshakeError::Binding(e) => write!(f, "{e}"),
224 HandshakeError::Key(e) => write!(f, "{e}"),
225 }
226 }
227}
228
229impl std::error::Error for HandshakeError {}
230
231impl From<BindingError> for HandshakeError {
232 fn from(e: BindingError) -> Self {
233 HandshakeError::Binding(e)
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum PuzzleMode {
240 Off,
242 LogOnly,
244 Enforce,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum PuzzleResult {
251 Solved,
252 Unsolved,
253 NotChecked,
254}
255
256pub fn opener() -> Vec<u8> {
259 encode_frame("opener", Vec::new())
260}
261
262pub fn read_opener(frame: &[u8]) -> Result<(), HandshakeError> {
264 decode(frame, "opener", &[OPENER_KEYS]).map(|_| ())
265}
266
267#[derive(Debug, Clone)]
270pub struct StationMaterial {
271 pub profile: Profile,
272 pub identity_key: Vec<u8>,
273 pub tls_binding: SignedTbs,
274 pub tls_status: SignedTbs,
275}
276
277pub fn challenge(m: &StationMaterial) -> Result<Vec<u8>, HandshakeError> {
280 let mut nonce = [0u8; NONCE_SIZE];
281 aws_lc_rs::rand::fill(&mut nonce)
282 .map_err(|_| HandshakeError::Key(KeyError::RandomnessUnavailable))?;
283 Ok(encode_frame(
284 "challenge",
285 vec![
286 entry("nonce", Value::Bytes(nonce.to_vec())),
287 entry("profile", Value::text(m.profile.name())),
288 entry("identity_key", Value::Bytes(m.identity_key.clone())),
289 entry("tls_binding", m.tls_binding.to_value()),
290 entry("tls_status", m.tls_status.to_value()),
291 ],
292 ))
293}
294
295pub struct ClientSession<'a> {
303 pub profile: Profile,
304 pub expected_node_id: [u8; 32],
305 pub leaf: &'a [u8],
306 pub identity_key: Vec<u8>,
307 pub connect_key: &'a NodeKey,
308 pub connect_binding: &'a SignedTbs,
309 pub connect_status: &'a SignedTbs,
310 pub capabilities: u64,
311 pub now_ms: i64,
312 pub member_endorsement: Vec<u8>,
313 pub version: i64,
314 pub export: Option<&'a Exporter>,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct Station {
322 pub node_id: [u8; 32],
323 pub identity_key: Vec<u8>,
324 pub tls_binding: SignedTbs,
325 pub status_expires_at: i64,
326 pub binding_not_after: i64,
327 pub version: i64,
328 profile: Profile,
329 exporter_value: Vec<u8>,
330 challenge: Vec<u8>,
331 connect: Vec<u8>,
332 client_node_id: [u8; 32],
333}
334
335pub fn answer_challenge(
341 challenge: &[u8],
342 s: &ClientSession<'_>,
343) -> Result<(Vec<u8>, Station), HandshakeError> {
344 let f = decode(challenge, "challenge", &[CHALLENGE_KEYS])?;
345 let station_key = f.bytes("identity_key");
346 let connect_key = s.connect_key.public_key();
347 if f.text("profile") != s.profile.name() {
348 return Err(HandshakeError::ProfileMismatch);
349 }
350 if !carried_key_well_formed(station_key, s.profile) {
351 return Err(HandshakeError::Malformed);
352 }
353 if shares_a_half(&s.identity_key, &connect_key)
354 || in_leaf(station_key, s.leaf)
355 || in_leaf(&connect_key, s.leaf)
356 {
357 return Err(HandshakeError::KeyPurposeReuse);
358 }
359 let station_node_id = node_id_of(station_key, s.profile);
360 if station_node_id != s.expected_node_id {
361 return Err(HandshakeError::PeerIdentityMismatch {
362 expected: s.expected_node_id,
363 derived: station_node_id,
364 });
365 }
366 let tls_binding = f.signed("tls_binding");
367 let binding = verify_tls_binding(&tls_binding, station_key, s.profile, s.leaf, s.now_ms)?;
368 let expires_at = verify_status(
369 &f.signed("tls_status"),
370 &tls_binding,
371 station_key,
372 s.profile,
373 s.now_ms,
374 )?;
375 let client_node_id = node_id_of(&s.identity_key, s.profile);
376 let e = session_exported(s.version, s.export, &client_node_id, &station_node_id)?;
377 let proof = s
378 .connect_key
379 .sign(&proof_message(
380 s.version,
381 f.bytes("nonce"),
382 &station_node_id,
383 &client_node_id,
384 s.leaf,
385 challenge,
386 &e,
387 s.capabilities,
388 ))
389 .map_err(HandshakeError::Key)?;
390 let connect = encode_frame_version(
391 s.version,
392 "connect",
393 vec![
394 entry("identity_key", Value::Bytes(s.identity_key.clone())),
395 entry("connect_key", Value::Bytes(connect_key)),
396 entry("connect_binding", s.connect_binding.to_value()),
397 entry("connect_status", s.connect_status.to_value()),
398 entry("proof", Value::Bytes(proof)),
399 entry("capabilities", Value::Int(i128::from(s.capabilities))),
400 entry(
401 "member_endorsement",
402 Value::Bytes(s.member_endorsement.clone()),
403 ),
404 ],
405 );
406 Ok((
407 connect.clone(),
408 Station {
409 node_id: station_node_id,
410 identity_key: station_key.to_vec(),
411 tls_binding,
412 status_expires_at: expires_at,
413 binding_not_after: binding.not_after,
414 version: s.version,
415 profile: s.profile,
416 exporter_value: e,
417 challenge: challenge.to_vec(),
418 connect,
419 client_node_id,
420 },
421 ))
422}
423
424fn session_exported(
427 version: i64,
428 export: Option<&Exporter>,
429 client_node_id: &[u8; 32],
430 station_node_id: &[u8; 32],
431) -> Result<Vec<u8>, HandshakeError> {
432 match (version, export) {
433 (VERSION, _) => Ok(Vec::new()),
434 (VERSION_5, Some(export)) => {
435 let context = [client_node_id.as_slice(), station_node_id].concat();
436 match export(EXPORTER_LABEL, &context, EXPORTER_SIZE) {
437 Some(e) if e.len() == EXPORTER_SIZE => Ok(e),
438 _ => Err(HandshakeError::ExporterUnavailable),
439 }
440 }
441 (VERSION_5, None) => Err(HandshakeError::ExporterUnavailable),
442 _ => Err(HandshakeError::UnsupportedVersion),
443 }
444}
445
446#[derive(Debug, Clone)]
451pub struct StationSession {
452 pub profile: Profile,
453 pub challenge: Vec<u8>,
454 pub leaf: Vec<u8>,
455 pub puzzle_difficulty: u32,
456 pub puzzle_mode: PuzzleMode,
457 pub capabilities: u64,
458 pub now_ms: i64,
459 pub v5: Option<StationV5>,
460}
461
462pub type SessionProofSigner =
466 dyn Fn(&[u8; 32], &[u8]) -> Result<Vec<u8>, HandshakeError> + Send + Sync;
467
468#[derive(Clone)]
471pub struct StationV5 {
472 pub export: Arc<Exporter>,
473 pub sign_session_proof: Arc<SessionProofSigner>,
474}
475
476impl fmt::Debug for StationV5 {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 f.write_str("StationV5")
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct Client {
485 pub node_id: [u8; 32],
486 pub identity_key: Vec<u8>,
487 pub connect_key: Vec<u8>,
488 pub connect_binding: SignedTbs,
489 pub capabilities: u64,
490 pub status_expires_at: i64,
491 pub binding_not_after: i64,
492 pub puzzle: PuzzleResult,
493 pub member_endorsement: Vec<u8>,
496 pub version: i64,
498}
499
500pub fn accept_connect(
513 connect: &[u8],
514 s: &StationSession,
515) -> (Result<Client, HandshakeError>, Vec<u8>) {
516 let mut version = VERSION;
517 match check_connect(connect, s, &mut version) {
518 Ok((client, session_proof)) => {
519 let hello = hello(client.version, None, s.capabilities, session_proof);
520 (Ok(client), hello)
521 }
522 Err(e) => {
523 let code = match e {
524 HandshakeError::UnsupportedVersion => RefusalCode::UnsupportedVersion,
525 HandshakeError::PuzzleInvalid => RefusalCode::PuzzleInvalid,
526 HandshakeError::SessionProofRate => RefusalCode::SessionProofRate,
527 _ => RefusalCode::NotAccepted,
528 };
529 (Err(e), hello(version, Some(code), s.capabilities, None))
530 }
531 }
532}
533
534fn check_connect(
537 connect: &[u8],
538 s: &StationSession,
539 version: &mut i64,
540) -> Result<(Client, Option<Vec<u8>>), HandshakeError> {
541 if s.puzzle_difficulty > 256 {
542 return Err(HandshakeError::InvalidStationSession);
543 }
544 let versions: &[i64] = match s.v5 {
545 Some(_) => &[VERSION, VERSION_5],
546 None => &[VERSION],
547 };
548 let f = decode_versioned(connect, "connect", versions, &[CONNECT_KEYS])?;
549 *version = f.version();
550 let (identity_key, connect_key, proof) = (
551 f.bytes("identity_key"),
552 f.bytes("connect_key"),
553 f.bytes("proof"),
554 );
555 if !carried_key_well_formed(identity_key, s.profile)
556 || !carried_key_well_formed(connect_key, s.profile)
557 || proof.len() != signature_size(s.profile)
558 {
559 return Err(HandshakeError::Malformed);
560 }
561 if shares_a_half(identity_key, connect_key) || in_leaf(connect_key, &s.leaf) {
562 return Err(HandshakeError::KeyPurposeReuse);
563 }
564 let node_id = node_id_of(identity_key, s.profile);
565 let puzzle = match s.puzzle_mode {
566 PuzzleMode::Off => PuzzleResult::NotChecked,
567 _ if puzzle_solved(&node_id, s.puzzle_difficulty) => PuzzleResult::Solved,
568 _ => PuzzleResult::Unsolved,
569 };
570 if puzzle == PuzzleResult::Unsolved && s.puzzle_mode == PuzzleMode::Enforce {
571 return Err(HandshakeError::PuzzleInvalid);
572 }
573 let connect_binding = f.signed("connect_binding");
574 let binding = verify_connect_binding(
575 &connect_binding,
576 identity_key,
577 s.profile,
578 connect_key,
579 s.now_ms,
580 )?;
581 let expires_at = verify_status(
582 &f.signed("connect_status"),
583 &connect_binding,
584 identity_key,
585 s.profile,
586 s.now_ms,
587 )?;
588 let challenge = decode(&s.challenge, "challenge", &[CHALLENGE_KEYS])
590 .map_err(|_| HandshakeError::ProofInvalid)?;
591 let station_node_id = node_id_of(challenge.bytes("identity_key"), s.profile);
592 let export = s.v5.as_ref().map(|v5| v5.export.as_ref());
593 let e = session_exported(*version, export, &node_id, &station_node_id)?;
594 let capabilities = f.uint("capabilities");
595 let message = proof_message(
596 *version,
597 challenge.bytes("nonce"),
598 &station_node_id,
599 &node_id,
600 &s.leaf,
601 &s.challenge,
602 &e,
603 capabilities,
604 );
605 if !verify(&message, proof, connect_key, s.profile) {
606 return Err(HandshakeError::ProofInvalid);
607 }
608 let session_proof = match (&s.v5, *version) {
609 (Some(v5), VERSION_5) => Some((v5.sign_session_proof)(
610 &node_id,
611 &session_proof_message(
612 &e,
613 &s.challenge,
614 connect,
615 &station_node_id,
616 &node_id,
617 s.capabilities,
618 ),
619 )?),
620 _ => None,
621 };
622 Ok((
623 Client {
624 node_id,
625 identity_key: identity_key.to_vec(),
626 connect_key: connect_key.to_vec(),
627 connect_binding,
628 capabilities,
629 status_expires_at: expires_at,
630 binding_not_after: binding.not_after,
631 puzzle,
632 member_endorsement: f.bytes("member_endorsement").to_vec(),
633 version: *version,
634 },
635 session_proof,
636 ))
637}
638
639#[allow(clippy::too_many_arguments)]
645fn proof_message(
646 version: i64,
647 nonce: &[u8],
648 station_node_id: &[u8; 32],
649 client_node_id: &[u8; 32],
650 leaf: &[u8],
651 challenge: &[u8],
652 e: &[u8],
653 capabilities: u64,
654) -> Vec<u8> {
655 let label = match version {
656 VERSION_5 => CONNECT_PROOF_LABEL_V2,
657 _ => CONNECT_PROOF_LABEL,
658 };
659 let mut out = Vec::with_capacity(label.len() + 1 + nonce.len() + 64 + 96 + EXPORTER_SIZE + 8);
660 out.extend_from_slice(label);
661 out.push(0);
662 out.extend_from_slice(nonce);
663 out.extend_from_slice(station_node_id);
664 out.extend_from_slice(client_node_id);
665 out.extend_from_slice(&Sha384::digest(leaf));
666 out.extend_from_slice(&Sha384::digest(challenge));
667 if version == VERSION_5 {
668 out.extend_from_slice(e);
669 out.extend_from_slice(&capabilities.to_be_bytes());
670 }
671 out
672}
673
674fn session_proof_message(
679 e: &[u8],
680 challenge: &[u8],
681 connect: &[u8],
682 station_node_id: &[u8; 32],
683 client_node_id: &[u8; 32],
684 capabilities: u64,
685) -> Vec<u8> {
686 let mut out = Vec::with_capacity(SESSION_PROOF_LABEL.len() + 1 + EXPORTER_SIZE + 96 + 64 + 8);
687 out.extend_from_slice(SESSION_PROOF_LABEL);
688 out.push(0);
689 out.extend_from_slice(e);
690 out.extend_from_slice(&Sha384::digest(challenge));
691 out.extend_from_slice(&Sha384::digest(connect));
692 out.extend_from_slice(station_node_id);
693 out.extend_from_slice(client_node_id);
694 out.extend_from_slice(&capabilities.to_be_bytes());
695 out
696}
697
698fn hello(
699 version: i64,
700 refusal: Option<RefusalCode>,
701 capabilities: u64,
702 session_proof: Option<Vec<u8>>,
703) -> Vec<u8> {
704 let mut entries = vec![
705 entry("accepted", Value::Int(i128::from(refusal.is_none()))),
706 entry("capabilities", Value::Int(i128::from(capabilities))),
707 ];
708 if let Some(code) = refusal {
709 entries.push(entry("refusal_code", Value::text(code.name())));
710 }
711 if let Some(proof) = session_proof {
712 entries.push(entry("session_proof", Value::Bytes(proof)));
713 }
714 encode_frame_version(version, "hello", entries)
715}
716
717pub fn read_hello(frame: &[u8], station: &Station) -> Result<u64, HandshakeError> {
725 let (versions, layouts): (&[i64], &[&[&str]]) = match station.version {
726 VERSION_5 => (
727 &[VERSION, VERSION_5],
728 &[HELLO_PROVED_KEYS, HELLO_ACCEPTED_KEYS, HELLO_REFUSED_KEYS],
729 ),
730 _ => (&[VERSION], &[HELLO_ACCEPTED_KEYS, HELLO_REFUSED_KEYS]),
731 };
732 let f = decode_versioned(frame, "hello", versions, layouts)?;
733 let refusal = f.0.get("refusal_code");
734 let proof = f.0.get("session_proof");
735 match (f.int("accepted"), refusal) {
736 (0, Some(Value::Text(code))) => {
737 return Err(HandshakeError::Refused(
738 RefusalCode::parse(code).ok_or(HandshakeError::Malformed)?,
739 ))
740 }
741 (1, None) => {}
742 _ => return Err(HandshakeError::Malformed),
743 }
744 let capabilities = f.uint("capabilities");
745 match (f.version(), station.version, proof) {
746 (VERSION, VERSION_5, _) => Err(HandshakeError::V4HelloToV5Connect),
747 (VERSION, _, None) => Ok(capabilities),
748 (VERSION_5, VERSION_5, None) => Err(HandshakeError::SessionProofMissing),
749 (VERSION_5, VERSION_5, Some(Value::Bytes(proof))) => {
750 if proof.len() != signature_size(station.profile) {
751 return Err(HandshakeError::Malformed);
752 }
753 let message = session_proof_message(
754 &station.exporter_value,
755 &station.challenge,
756 &station.connect,
757 &station.node_id,
758 &station.client_node_id,
759 capabilities,
760 );
761 if !verify(&message, proof, &station.identity_key, station.profile) {
762 return Err(HandshakeError::SessionProofInvalid);
763 }
764 Ok(capabilities)
765 }
766 _ => Err(HandshakeError::Malformed),
767 }
768}
769
770pub fn status_frame(statement: &SignedTbs) -> Vec<u8> {
772 encode_frame("status", vec![entry("statement", statement.to_value())])
773}
774
775#[derive(Debug, Clone)]
779pub struct Peer {
780 pub profile: Profile,
781 pub identity_key: Vec<u8>,
782 pub binding: SignedTbs,
783 pub now_ms: i64,
784}
785
786pub fn read_status(frame: &[u8], p: &Peer) -> Result<i64, HandshakeError> {
788 let f = decode(frame, "status", &[STATUS_KEYS])?;
789 Ok(verify_status(
790 &f.signed("statement"),
791 &p.binding,
792 &p.identity_key,
793 p.profile,
794 p.now_ms,
795 )?)
796}
797
798struct Fields(std::collections::HashMap<String, Value>);
800
801impl Fields {
802 fn bytes(&self, key: &str) -> &[u8] {
803 match self.0.get(key) {
804 Some(Value::Bytes(b)) => b,
805 _ => &[],
806 }
807 }
808
809 fn text(&self, key: &str) -> &str {
810 match self.0.get(key) {
811 Some(Value::Text(t)) => t,
812 _ => "",
813 }
814 }
815
816 fn int(&self, key: &str) -> i128 {
817 match self.0.get(key) {
818 Some(Value::Int(n)) => *n,
819 _ => -1,
820 }
821 }
822
823 fn uint(&self, key: &str) -> u64 {
824 u64::try_from(self.int(key)).unwrap_or(0)
825 }
826
827 fn version(&self) -> i64 {
828 i64::try_from(self.int("version")).unwrap_or(-1)
829 }
830
831 fn signed(&self, key: &str) -> SignedTbs {
832 self.0
833 .get(key)
834 .and_then(|v| SignedTbs::from_value(v).ok())
835 .unwrap_or(SignedTbs {
836 tbs: Vec::new(),
837 signature: Vec::new(),
838 })
839 }
840}
841
842fn decode(frame: &[u8], frame_type: &str, layouts: &[&[&str]]) -> Result<Fields, HandshakeError> {
846 decode_versioned(frame, frame_type, &[VERSION], layouts)
847}
848
849fn decode_versioned(
852 frame: &[u8],
853 frame_type: &str,
854 versions: &[i64],
855 layouts: &[&[&str]],
856) -> Result<Fields, HandshakeError> {
857 let Ok(Value::Map(pairs)) = cbor::decode(frame) else {
858 return Err(HandshakeError::Malformed);
859 };
860 let mut fields = std::collections::HashMap::with_capacity(pairs.len());
861 let mut non_text = 0;
862 for (key, value) in pairs {
863 match key {
864 Value::Text(name) => {
865 fields.insert(name, value);
866 }
867 _ => non_text += 1,
868 }
869 }
870 match fields.get("version") {
871 Some(Value::Int(v)) if versions.iter().any(|known| *v == i128::from(*known)) => {}
872 Some(Value::Int(_)) => return Err(HandshakeError::UnsupportedVersion),
873 _ => return Err(HandshakeError::Malformed),
874 }
875 match fields.get("frame_type") {
876 Some(Value::Text(t)) if t == frame_type => {}
877 Some(Value::Text(_)) => return Err(HandshakeError::UnexpectedFrame),
878 _ => return Err(HandshakeError::Malformed),
879 }
880 let mut keys: Vec<&str> = fields.keys().map(String::as_str).collect();
881 keys.sort_unstable();
882 let has_layout = layouts.contains(&keys.as_slice());
883 if non_text > 0 || !has_layout || !fields.iter().all(|(k, v)| field_typed(k, v)) {
884 return Err(HandshakeError::Malformed);
885 }
886 Ok(Fields(fields))
887}
888
889fn field_typed(key: &str, v: &Value) -> bool {
890 match key {
891 "version" | "frame_type" => true,
892 "profile" => matches!(v, Value::Text(_)),
893 "nonce" => matches!(v, Value::Bytes(b) if b.len() == NONCE_SIZE),
894 "identity_key" | "connect_key" | "proof" | "member_endorsement" | "session_proof" => {
895 matches!(v, Value::Bytes(_))
896 }
897 "tls_binding" | "tls_status" | "connect_binding" | "connect_status" | "statement" => {
898 SignedTbs::from_value(v).is_ok()
899 }
900 "capabilities" => {
901 matches!(v, Value::Int(n) if *n >= 0 && *n < i128::from(MAX_PROTOCOL_INT))
902 }
903 "accepted" => matches!(v, Value::Int(0 | 1)),
904 "refusal_code" => matches!(v, Value::Text(t) if RefusalCode::parse(t).is_some()),
905 _ => false,
906 }
907}
908
909fn in_leaf(key: &[u8], leaf: &[u8]) -> bool {
911 key.len() >= MLDSA_KEY_SIZE
912 && leaf
913 .windows(MLDSA_KEY_SIZE)
914 .any(|w| w == &key[..MLDSA_KEY_SIZE])
915}
916
917fn shares_a_half(a: &[u8], b: &[u8]) -> bool {
919 if a.len() < MLDSA_KEY_SIZE || b.len() < MLDSA_KEY_SIZE {
920 return a == b;
921 }
922 let (classical_a, classical_b) = (&a[MLDSA_KEY_SIZE..], &b[MLDSA_KEY_SIZE..]);
923 a[..MLDSA_KEY_SIZE] == b[..MLDSA_KEY_SIZE]
924 || (!classical_a.is_empty() && classical_a == classical_b)
925}
926
927fn encode_frame(frame_type: &str, entries: Vec<(Value, Value)>) -> Vec<u8> {
928 encode_frame_version(VERSION, frame_type, entries)
929}
930
931fn encode_frame_version(version: i64, frame_type: &str, entries: Vec<(Value, Value)>) -> Vec<u8> {
934 let mut all = vec![
935 entry("version", Value::Int(i128::from(version))),
936 entry("frame_type", Value::text(frame_type)),
937 ];
938 all.extend(entries);
939 cbor::encode(&Value::Map(all)).expect("a handshake frame's integers are all below 2^53")
940}
941
942fn entry(key: &str, value: Value) -> (Value, Value) {
943 (Value::text(key), value)
944}
945
946fn hex_of(bytes: &[u8]) -> String {
947 bytes.iter().map(|b| format!("{b:02x}")).collect()
948}