1use std::fmt;
31use std::sync::Arc;
32
33use sha2::{Digest, Sha384};
34
35use crate::binding::{
36 verify_connect_binding, verify_status, verify_tls_binding, BindingError, SignedTbs,
37};
38use crate::cbor::{self, Value};
39use crate::node_key::{
40 carried_key_well_formed, node_id_of, puzzle_solved, signature_size, verify, KeyError, NodeKey,
41};
42use crate::profile::Profile;
43
44#[cfg(test)]
45mod tests;
46#[cfg(test)]
47mod wire_1_0_tests;
48
49pub const VERSION: i64 = 5;
52
53pub const EXPORTER_LABEL: &str = "EXPORTER-macula-session-v1";
56
57pub const CAP_STATION: u64 = 1 << 0;
63pub const CAP_SWIM_INDIRECT: u64 = 1 << 1;
65pub const CAP_RELAY_OVERLOADED: u64 = 1 << 2;
68pub const MAX_CAPABILITY_BIT: u32 = 52;
70
71const NONCE_SIZE: usize = 32;
72const EXPORTER_SIZE: usize = 32;
73const MAX_PROTOCOL_INT: i128 = 1 << 53;
74const MLDSA_KEY_SIZE: usize = 2592;
75const MAX_PROFILES: usize = 8;
76const MAX_REFUSAL_CODE_BYTES: usize = 64;
77const CONNECT_PROOF_LABEL: &[u8] = b"MACULA-PQ-CONNECT-PROOF-V3";
78const SESSION_PROOF_LABEL: &[u8] = b"MACULA-PQ-SESSION-PROOF-V2";
79
80pub type Exporter = dyn Fn(&str, &[u8], usize) -> Option<Vec<u8>> + Send + Sync;
84
85pub type Floor = dyn Fn(&[u8; 32]) -> i64 + Send + Sync;
89
90const OPENER_KEYS: &[&str] = &["frame_type", "profiles", "version", "versions"];
91const CHALLENGE_KEYS: &[&str] = &[
92 "frame_type",
93 "identity_key",
94 "nonce",
95 "profile",
96 "profiles",
97 "tls_binding",
98 "tls_status",
99 "version",
100 "versions",
101];
102const CONNECT_KEYS: &[&str] = &[
106 "capabilities",
107 "connect_binding",
108 "connect_key",
109 "connect_status",
110 "frame_type",
111 "identity_key",
112 "member_endorsement",
113 "profile",
114 "profiles",
115 "proof",
116 "version",
117 "versions",
118];
119const HELLO_PROVED_KEYS: &[&str] = &[
120 "accepted",
121 "capabilities",
122 "frame_type",
123 "session_proof",
124 "version",
125];
126const HELLO_ACCEPTED_KEYS: &[&str] = &["accepted", "capabilities", "frame_type", "version"];
127const HELLO_REFUSED_KEYS: &[&str] = &[
128 "accepted",
129 "capabilities",
130 "frame_type",
131 "refusal_code",
132 "version",
133];
134const HELLO_EARLY_KEYS: &[&str] = &[
135 "accepted",
136 "frame_type",
137 "profiles",
138 "refusal_code",
139 "version",
140 "versions",
141];
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum RefusalCode {
146 UnsupportedVersion,
148 ProfileUnsupported,
150 PuzzleInvalid,
152 SessionProofRate,
154 NotAccepted,
157}
158
159impl RefusalCode {
160 const ALL: [RefusalCode; 5] = [
161 RefusalCode::UnsupportedVersion,
162 RefusalCode::ProfileUnsupported,
163 RefusalCode::PuzzleInvalid,
164 RefusalCode::SessionProofRate,
165 RefusalCode::NotAccepted,
166 ];
167
168 pub fn name(self) -> &'static str {
169 match self {
170 RefusalCode::UnsupportedVersion => "unsupported_version",
171 RefusalCode::ProfileUnsupported => "profile_unsupported",
172 RefusalCode::PuzzleInvalid => "puzzle_invalid",
173 RefusalCode::SessionProofRate => "session_proof_rate",
174 RefusalCode::NotAccepted => "not_accepted",
175 }
176 }
177
178 fn read(name: &str) -> RefusalCode {
181 RefusalCode::ALL
182 .into_iter()
183 .find(|code| code.name() == name)
184 .unwrap_or_else(|| {
185 crate::frame::count_enum_fallback("refusal_code");
186 RefusalCode::NotAccepted
187 })
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Ranges {
196 pub min_version: i64,
197 pub max_version: i64,
198 pub profiles: Vec<String>,
199}
200
201impl Ranges {
202 pub fn supported(profile: Profile) -> Ranges {
205 Ranges {
206 min_version: VERSION,
207 max_version: VERSION,
208 profiles: vec![profile.name().to_string()],
209 }
210 }
211
212 fn entries(&self) -> Vec<(Value, Value)> {
213 vec![
214 entry(
215 "versions",
216 Value::List(vec![
217 Value::Int(i128::from(self.min_version)),
218 Value::Int(i128::from(self.max_version)),
219 ]),
220 ),
221 entry(
222 "profiles",
223 Value::List(self.profiles.iter().map(Value::text).collect()),
224 ),
225 ]
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum HandshakeError {
233 UnexpectedFrame,
235 UnsupportedVersion,
238 ProfileUnsupported,
241 DowngradeDetected,
245 NegotiationMismatch,
248 VersionBelowFloor,
251 Malformed,
254 KeyPurposeReuse,
257 PeerIdentityMismatch {
259 expected: [u8; 32],
260 derived: [u8; 32],
261 },
262 PuzzleInvalid,
264 ProofInvalid,
266 Refused {
269 code: RefusalCode,
270 station_ranges: Option<Ranges>,
271 },
272 InvalidStationSession,
274 SessionProofInvalid,
277 SessionProofMissing,
279 SessionProofRate,
281 ExporterUnavailable,
283 Binding(BindingError),
285 Key(KeyError),
287}
288
289impl fmt::Display for HandshakeError {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 HandshakeError::UnexpectedFrame => f.write_str("unexpected frame"),
293 HandshakeError::UnsupportedVersion => f.write_str("unsupported_version"),
294 HandshakeError::ProfileUnsupported => f.write_str("profile_unsupported"),
295 HandshakeError::DowngradeDetected => f.write_str("downgrade_detected"),
296 HandshakeError::NegotiationMismatch => f.write_str("negotiation_mismatch"),
297 HandshakeError::VersionBelowFloor => f.write_str("version_below_floor"),
298 HandshakeError::Malformed => f.write_str("malformed frame"),
299 HandshakeError::KeyPurposeReuse => f.write_str("a key would serve two purposes"),
300 HandshakeError::PeerIdentityMismatch { expected, derived } => write!(
301 f,
302 "dialed node_id {}, but the station's key derives {}",
303 hex_of(expected),
304 hex_of(derived)
305 ),
306 HandshakeError::PuzzleInvalid => f.write_str("the node_id does not meet the puzzle"),
307 HandshakeError::ProofInvalid => f.write_str("the CONNECT proof does not verify"),
308 HandshakeError::Refused { code, .. } => {
309 write!(f, "the station refused the connection: {}", code.name())
310 }
311 HandshakeError::InvalidStationSession => {
312 f.write_str("the station session has an unknown puzzle difficulty")
313 }
314 HandshakeError::SessionProofInvalid => f.write_str("the session proof does not verify"),
315 HandshakeError::SessionProofMissing => {
316 f.write_str("the HELLO carries no session proof")
317 }
318 HandshakeError::SessionProofRate => {
319 f.write_str("the station's session proof budget is spent")
320 }
321 HandshakeError::ExporterUnavailable => f.write_str("the TLS exporter is unavailable"),
322 HandshakeError::Binding(e) => write!(f, "{e}"),
323 HandshakeError::Key(e) => write!(f, "{e}"),
324 }
325 }
326}
327
328impl std::error::Error for HandshakeError {}
329
330impl From<BindingError> for HandshakeError {
331 fn from(e: BindingError) -> Self {
332 HandshakeError::Binding(e)
333 }
334}
335
336impl HandshakeError {
337 fn wire_refusal(&self) -> RefusalCode {
339 match self {
340 HandshakeError::UnsupportedVersion => RefusalCode::UnsupportedVersion,
341 HandshakeError::PuzzleInvalid => RefusalCode::PuzzleInvalid,
342 HandshakeError::SessionProofRate => RefusalCode::SessionProofRate,
343 _ => RefusalCode::NotAccepted,
344 }
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum PuzzleMode {
351 Off,
353 LogOnly,
355 Enforce,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361pub enum PuzzleResult {
362 Solved,
363 Unsolved,
364 NotChecked,
365}
366
367fn negotiate(client: &Ranges, station: &Ranges) -> Result<(i64, String), HandshakeError> {
371 let version = client.max_version.min(station.max_version).min(VERSION);
372 if version < client.min_version.max(station.min_version) {
373 return Err(HandshakeError::UnsupportedVersion);
374 }
375 client
376 .profiles
377 .iter()
378 .find(|p| station.profiles.contains(p))
379 .map(|p| (version, p.clone()))
380 .ok_or(HandshakeError::ProfileUnsupported)
381}
382
383pub fn opener(ranges: &Ranges) -> Vec<u8> {
386 encode_frame("opener", ranges.entries())
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
392pub struct Negotiated {
393 pub version: i64,
394 pub profile: String,
395 pub client: Ranges,
396}
397
398pub fn read_opener(
406 opener: &[u8],
407 station: &Ranges,
408) -> (Result<Negotiated, HandshakeError>, Option<Vec<u8>>) {
409 let read = decode(opener, "opener", &[OPENER_KEYS]).and_then(|f| {
410 let client = f.ranges();
411 let (version, profile) = negotiate(&client, station)?;
412 Ok(Negotiated {
413 version,
414 profile,
415 client,
416 })
417 });
418 match &read {
419 Err(e @ (HandshakeError::UnsupportedVersion | HandshakeError::ProfileUnsupported)) => {
420 let hello = hello_early_refusal(e, station);
421 (read, Some(hello))
422 }
423 _ => (read, None),
424 }
425}
426
427#[derive(Debug, Clone)]
430pub struct StationMaterial {
431 pub profile: Profile,
432 pub identity_key: Vec<u8>,
433 pub tls_binding: SignedTbs,
434 pub tls_status: SignedTbs,
435}
436
437pub fn challenge(
441 m: &StationMaterial,
442 n: &Negotiated,
443 station: &Ranges,
444) -> Result<Vec<u8>, HandshakeError> {
445 let mut nonce = [0u8; NONCE_SIZE];
446 aws_lc_rs::rand::fill(&mut nonce)
447 .map_err(|_| HandshakeError::Key(KeyError::RandomnessUnavailable))?;
448 let mut entries = station.entries();
449 entries.extend([
450 entry("nonce", Value::Bytes(nonce.to_vec())),
451 entry("profile", Value::text(n.profile.as_str())),
452 entry("identity_key", Value::Bytes(m.identity_key.clone())),
453 entry("tls_binding", m.tls_binding.to_value()),
454 entry("tls_status", m.tls_status.to_value()),
455 ]);
456 Ok(encode_frame("challenge", entries))
457}
458
459pub struct ClientSession<'a> {
466 pub profile: Profile,
467 pub expected_node_id: [u8; 32],
468 pub leaf: &'a [u8],
469 pub identity_key: Vec<u8>,
470 pub connect_key: &'a NodeKey,
471 pub connect_binding: &'a SignedTbs,
472 pub connect_status: &'a SignedTbs,
473 pub capabilities: u64,
474 pub now_ms: i64,
475 pub member_endorsement: Vec<u8>,
476 pub ranges: Ranges,
477 pub floor: &'a Floor,
478 pub export: &'a Exporter,
479}
480
481struct ChallengeCheck<'a> {
485 profile: Profile,
486 expected_node_id: [u8; 32],
487 leaf: &'a [u8],
488 identity_key: &'a [u8],
489 connect_public: &'a [u8],
490 now_ms: i64,
491 ranges: &'a Ranges,
492 floor: &'a Floor,
493 export: &'a Exporter,
494}
495
496#[derive(Debug, Clone, PartialEq, Eq)]
499pub struct Station {
500 pub node_id: [u8; 32],
501 pub identity_key: Vec<u8>,
502 pub tls_binding: SignedTbs,
503 pub status_expires_at: i64,
504 pub binding_not_after: i64,
505 pub version: i64,
507 profile: Profile,
508 nonce: Vec<u8>,
509 leaf: Vec<u8>,
510 exporter_value: Vec<u8>,
511 challenge: Vec<u8>,
512 connect: Vec<u8>,
513 client_node_id: [u8; 32],
514}
515
516impl Station {
517 fn proof_message(&self, connect_tbs: &[u8]) -> Vec<u8> {
519 proof_message(
520 &self.nonce,
521 &self.node_id,
522 &self.client_node_id,
523 &self.leaf,
524 &self.challenge,
525 &self.exporter_value,
526 connect_tbs,
527 )
528 }
529}
530
531fn read_challenge(challenge: &[u8], s: &ChallengeCheck<'_>) -> Result<Station, HandshakeError> {
539 let f = decode(challenge, "challenge", &[CHALLENGE_KEYS])
540 .map_err(|e| refusal_before_challenge(challenge).unwrap_or(e))?;
541 let negotiated = negotiate(s.ranges, &f.ranges()).ok();
542 let chosen = negotiated.filter(|(version, profile)| {
543 i128::from(*version) == f.int("version")
544 && profile == f.text("profile")
545 && profile == s.profile.name()
546 });
547 let Some((version, _)) = chosen else {
548 return Err(HandshakeError::DowngradeDetected);
549 };
550 if version < (s.floor)(&s.expected_node_id) {
551 return Err(HandshakeError::VersionBelowFloor);
552 }
553 let station_key = f.bytes("identity_key");
554 if !carried_key_well_formed(station_key, s.profile) {
555 return Err(HandshakeError::Malformed);
556 }
557 if shares_a_half(s.identity_key, s.connect_public)
558 || in_leaf(station_key, s.leaf)
559 || in_leaf(s.connect_public, s.leaf)
560 {
561 return Err(HandshakeError::KeyPurposeReuse);
562 }
563 let station_node_id = node_id_of(station_key, s.profile);
564 if station_node_id != s.expected_node_id {
565 return Err(HandshakeError::PeerIdentityMismatch {
566 expected: s.expected_node_id,
567 derived: station_node_id,
568 });
569 }
570 let tls_binding = f.signed("tls_binding");
571 let binding = verify_tls_binding(&tls_binding, station_key, s.profile, s.leaf, s.now_ms)?;
572 let status = f.signed("tls_status");
573 let expires_at = verify_status(&status, &tls_binding, station_key, s.profile, s.now_ms)?;
574 let client_node_id = node_id_of(s.identity_key, s.profile);
575 let e = session_exported(s.export, &client_node_id, &station_node_id)?;
576 Ok(Station {
577 node_id: station_node_id,
578 identity_key: station_key.to_vec(),
579 tls_binding,
580 status_expires_at: expires_at,
581 binding_not_after: binding.not_after,
582 version,
583 profile: s.profile,
584 nonce: f.bytes("nonce").to_vec(),
585 leaf: s.leaf.to_vec(),
586 exporter_value: e,
587 challenge: challenge.to_vec(),
588 connect: Vec::new(),
589 client_node_id,
590 })
591}
592
593pub fn answer_challenge(
597 challenge: &[u8],
598 s: &ClientSession<'_>,
599) -> Result<(Vec<u8>, Station), HandshakeError> {
600 let connect_public = s.connect_key.public_key();
601 let mut station = read_challenge(
602 challenge,
603 &ChallengeCheck {
604 profile: s.profile,
605 expected_node_id: s.expected_node_id,
606 leaf: s.leaf,
607 identity_key: &s.identity_key,
608 connect_public: &connect_public,
609 now_ms: s.now_ms,
610 ranges: &s.ranges,
611 floor: s.floor,
612 export: s.export,
613 },
614 )?;
615 let mut entries = s.ranges.entries();
616 entries.extend([
617 entry("profile", Value::text(s.profile.name())),
618 entry("identity_key", Value::Bytes(s.identity_key.clone())),
619 entry("connect_key", Value::Bytes(connect_public)),
620 entry("connect_binding", s.connect_binding.to_value()),
621 entry("connect_status", s.connect_status.to_value()),
622 entry("capabilities", Value::Int(i128::from(s.capabilities))),
623 entry(
624 "member_endorsement",
625 Value::Bytes(s.member_endorsement.clone()),
626 ),
627 ]);
628 let with_proof = |proof: Vec<u8>| {
629 let mut all = entries.clone();
630 all.push(entry("proof", Value::Bytes(proof)));
631 encode_frame("connect", all)
632 };
633 let tbs = with_proof(Vec::new());
634 let proof = s
635 .connect_key
636 .sign(&station.proof_message(&tbs))
637 .map_err(HandshakeError::Key)?;
638 station.connect = with_proof(proof);
639 Ok((station.connect.clone(), station))
640}
641
642fn session_exported(
645 export: &Exporter,
646 client_node_id: &[u8; 32],
647 station_node_id: &[u8; 32],
648) -> Result<Vec<u8>, HandshakeError> {
649 let context = [client_node_id.as_slice(), station_node_id].concat();
650 match export(EXPORTER_LABEL, &context, EXPORTER_SIZE) {
651 Some(e) if e.len() == EXPORTER_SIZE => Ok(e),
652 _ => Err(HandshakeError::ExporterUnavailable),
653 }
654}
655
656pub type SessionProofSigner =
660 dyn Fn(&[u8; 32], &[u8]) -> Result<Vec<u8>, HandshakeError> + Send + Sync;
661
662#[derive(Clone)]
668pub struct StationSession {
669 pub profile: Profile,
670 pub negotiated: Negotiated,
671 pub challenge: Vec<u8>,
672 pub leaf: Vec<u8>,
673 pub puzzle_difficulty: u32,
674 pub puzzle_mode: PuzzleMode,
675 pub capabilities: u64,
676 pub now_ms: i64,
677 pub floor: Arc<Floor>,
678 pub export: Arc<Exporter>,
679 pub sign_session_proof: Arc<SessionProofSigner>,
680}
681
682impl fmt::Debug for StationSession {
683 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684 f.debug_struct("StationSession")
685 .field("profile", &self.profile)
686 .field("negotiated", &self.negotiated)
687 .finish_non_exhaustive()
688 }
689}
690
691#[derive(Debug, Clone, PartialEq, Eq)]
693pub struct Client {
694 pub node_id: [u8; 32],
695 pub identity_key: Vec<u8>,
696 pub connect_key: Vec<u8>,
697 pub connect_binding: SignedTbs,
698 pub capabilities: u64,
699 pub status_expires_at: i64,
700 pub binding_not_after: i64,
701 pub puzzle: PuzzleResult,
702 pub member_endorsement: Vec<u8>,
705 pub version: i64,
707}
708
709pub fn accept_connect(
720 connect: &[u8],
721 s: &StationSession,
722) -> (Result<Client, HandshakeError>, Vec<u8>) {
723 match check_connect(connect, s) {
724 Ok((client, session_proof)) => {
725 let hello = hello_accepted(s.capabilities, &session_proof);
726 (Ok(client), hello)
727 }
728 Err(e) => {
729 let hello = hello_refused(e.wire_refusal());
730 (Err(e), hello)
731 }
732 }
733}
734
735fn check_connect(connect: &[u8], s: &StationSession) -> Result<(Client, Vec<u8>), HandshakeError> {
737 if s.puzzle_difficulty > 256 {
738 return Err(HandshakeError::InvalidStationSession);
739 }
740 let f = decode(connect, "connect", &[CONNECT_KEYS])?;
741 if f.ranges() != s.negotiated.client {
742 return Err(HandshakeError::NegotiationMismatch);
743 }
744 if f.text("profile") != s.negotiated.profile || s.negotiated.profile != s.profile.name() {
745 return Err(HandshakeError::ProfileUnsupported);
746 }
747 let (identity_key, connect_key, proof) = (
748 f.bytes("identity_key"),
749 f.bytes("connect_key"),
750 f.bytes("proof"),
751 );
752 check_carried_keys(identity_key, connect_key, proof, s)?;
753 let node_id = node_id_of(identity_key, s.profile);
754 let puzzle = puzzle_verdict(&node_id, s)?;
755 if s.negotiated.version < (s.floor)(&node_id) {
756 return Err(HandshakeError::VersionBelowFloor);
757 }
758 let (connect_binding, binding_not_after, expires_at) =
759 verified_connect_statements(&f, identity_key, connect_key, s)?;
760 let session_proof = proved_session(connect, proof, connect_key, &node_id, s)?;
761 Ok((
762 Client {
763 node_id,
764 identity_key: identity_key.to_vec(),
765 connect_key: connect_key.to_vec(),
766 connect_binding,
767 capabilities: f.uint("capabilities"),
768 status_expires_at: expires_at,
769 binding_not_after,
770 puzzle,
771 member_endorsement: f.bytes("member_endorsement").to_vec(),
772 version: s.negotiated.version,
773 },
774 session_proof,
775 ))
776}
777
778fn proved_session(
782 connect: &[u8],
783 proof: &[u8],
784 connect_key: &[u8],
785 node_id: &[u8; 32],
786 s: &StationSession,
787) -> Result<Vec<u8>, HandshakeError> {
788 let challenge = decode(&s.challenge, "challenge", &[CHALLENGE_KEYS])
789 .map_err(|_| HandshakeError::ProofInvalid)?;
790 let station_node_id = node_id_of(challenge.bytes("identity_key"), s.profile);
791 let e = session_exported(s.export.as_ref(), node_id, &station_node_id)?;
792 let tbs = frame_tbs(connect, "proof")?;
793 let message = proof_message(
794 challenge.bytes("nonce"),
795 &station_node_id,
796 node_id,
797 &s.leaf,
798 &s.challenge,
799 &e,
800 &tbs,
801 );
802 if !verify(&message, proof, connect_key, s.profile) {
803 return Err(HandshakeError::ProofInvalid);
804 }
805 let hello_tbs = hello_accepted(s.capabilities, &[]);
806 (s.sign_session_proof)(
807 node_id,
808 &session_proof_message(
809 &e,
810 &s.challenge,
811 connect,
812 &station_node_id,
813 node_id,
814 &hello_tbs,
815 ),
816 )
817}
818
819fn check_carried_keys(
822 identity_key: &[u8],
823 connect_key: &[u8],
824 proof: &[u8],
825 s: &StationSession,
826) -> Result<(), HandshakeError> {
827 if !carried_key_well_formed(identity_key, s.profile)
828 || !carried_key_well_formed(connect_key, s.profile)
829 || proof.len() != signature_size(s.profile)
830 {
831 return Err(HandshakeError::Malformed);
832 }
833 if shares_a_half(identity_key, connect_key) || in_leaf(connect_key, &s.leaf) {
834 return Err(HandshakeError::KeyPurposeReuse);
835 }
836 Ok(())
837}
838
839fn puzzle_verdict(node_id: &[u8; 32], s: &StationSession) -> Result<PuzzleResult, HandshakeError> {
842 let puzzle = match s.puzzle_mode {
843 PuzzleMode::Off => PuzzleResult::NotChecked,
844 _ if puzzle_solved(node_id, s.puzzle_difficulty) => PuzzleResult::Solved,
845 _ => PuzzleResult::Unsolved,
846 };
847 if puzzle == PuzzleResult::Unsolved && s.puzzle_mode == PuzzleMode::Enforce {
848 return Err(HandshakeError::PuzzleInvalid);
849 }
850 Ok(puzzle)
851}
852
853fn verified_connect_statements(
856 f: &Fields,
857 identity_key: &[u8],
858 connect_key: &[u8],
859 s: &StationSession,
860) -> Result<(SignedTbs, i64, i64), HandshakeError> {
861 let connect_binding = f.signed("connect_binding");
862 let binding = verify_connect_binding(
863 &connect_binding,
864 identity_key,
865 s.profile,
866 connect_key,
867 s.now_ms,
868 )?;
869 let expires_at = verify_status(
870 &f.signed("connect_status"),
871 &connect_binding,
872 identity_key,
873 s.profile,
874 s.now_ms,
875 )?;
876 Ok((connect_binding, binding.not_after, expires_at))
877}
878
879fn frame_tbs(raw: &[u8], signature_key: &str) -> Result<Vec<u8>, HandshakeError> {
885 let v = cbor::decode(raw).map_err(|_| HandshakeError::Malformed)?;
886 if cbor::encode(&v).ok().as_deref() != Some(raw) {
887 return Err(HandshakeError::Malformed);
888 }
889 let rebuilt = v.with_field(signature_key, Value::Bytes(Vec::new()));
890 cbor::encode(&rebuilt).map_err(|_| HandshakeError::Malformed)
891}
892
893fn proof_message(
899 nonce: &[u8],
900 station_node_id: &[u8; 32],
901 client_node_id: &[u8; 32],
902 leaf: &[u8],
903 challenge: &[u8],
904 e: &[u8],
905 connect_tbs: &[u8],
906) -> Vec<u8> {
907 let mut out = Vec::with_capacity(CONNECT_PROOF_LABEL.len() + 1 + NONCE_SIZE + 64 + 144 + 32);
908 out.extend_from_slice(CONNECT_PROOF_LABEL);
909 out.push(0);
910 out.extend_from_slice(nonce);
911 out.extend_from_slice(station_node_id);
912 out.extend_from_slice(client_node_id);
913 out.extend_from_slice(&Sha384::digest(leaf));
914 out.extend_from_slice(&Sha384::digest(challenge));
915 out.extend_from_slice(e);
916 out.extend_from_slice(&Sha384::digest(connect_tbs));
917 out
918}
919
920fn session_proof_message(
927 e: &[u8],
928 challenge: &[u8],
929 connect: &[u8],
930 station_node_id: &[u8; 32],
931 client_node_id: &[u8; 32],
932 hello_tbs: &[u8],
933) -> Vec<u8> {
934 let mut out = Vec::with_capacity(SESSION_PROOF_LABEL.len() + 1 + EXPORTER_SIZE + 144 + 64);
935 out.extend_from_slice(SESSION_PROOF_LABEL);
936 out.push(0);
937 out.extend_from_slice(e);
938 out.extend_from_slice(&Sha384::digest(challenge));
939 out.extend_from_slice(&Sha384::digest(connect));
940 out.extend_from_slice(station_node_id);
941 out.extend_from_slice(client_node_id);
942 out.extend_from_slice(&Sha384::digest(hello_tbs));
943 out
944}
945
946fn hello_accepted(capabilities: u64, session_proof: &[u8]) -> Vec<u8> {
947 encode_frame(
948 "hello",
949 vec![
950 entry("accepted", Value::Int(1)),
951 entry("capabilities", Value::Int(i128::from(capabilities))),
952 entry("session_proof", Value::Bytes(session_proof.to_vec())),
953 ],
954 )
955}
956
957fn hello_refused(code: RefusalCode) -> Vec<u8> {
958 encode_frame(
959 "hello",
960 vec![
961 entry("accepted", Value::Int(0)),
962 entry("refusal_code", Value::text(code.name())),
963 entry("capabilities", Value::Int(0)),
964 ],
965 )
966}
967
968fn hello_early_refusal(e: &HandshakeError, station: &Ranges) -> Vec<u8> {
971 let code = match e {
972 HandshakeError::UnsupportedVersion => RefusalCode::UnsupportedVersion,
973 _ => RefusalCode::ProfileUnsupported,
974 };
975 let mut entries = station.entries();
976 entries.extend([
977 entry("accepted", Value::Int(0)),
978 entry("refusal_code", Value::text(code.name())),
979 ]);
980 encode_frame("hello", entries)
981}
982
983pub fn read_hello(hello: &[u8], station: &Station) -> Result<u64, HandshakeError> {
989 let f = decode(
990 hello,
991 "hello",
992 &[
993 HELLO_PROVED_KEYS,
994 HELLO_ACCEPTED_KEYS,
995 HELLO_REFUSED_KEYS,
996 HELLO_EARLY_KEYS,
997 ],
998 )?;
999 let accepted = f.int("accepted");
1000 let code =
1001 f.0.get("refusal_code")
1002 .map(|_| RefusalCode::read(f.text("refusal_code")));
1003 let early = f.0.contains_key("versions");
1004 match (accepted, code, early) {
1005 (0, Some(code), true) => {
1006 return Err(HandshakeError::Refused {
1007 code,
1008 station_ranges: Some(f.ranges()),
1009 })
1010 }
1011 (0, Some(code), false) => {
1012 return Err(HandshakeError::Refused {
1013 code,
1014 station_ranges: None,
1015 })
1016 }
1017 (1, None, false) => {}
1018 _ => return Err(HandshakeError::Malformed),
1019 }
1020 let Some(Value::Bytes(proof)) = f.0.get("session_proof") else {
1021 return Err(HandshakeError::SessionProofMissing);
1022 };
1023 if proof.len() != signature_size(station.profile) {
1024 return Err(HandshakeError::Malformed);
1025 }
1026 let tbs = frame_tbs(hello, "session_proof")?;
1027 let message = session_proof_message(
1028 &station.exporter_value,
1029 &station.challenge,
1030 &station.connect,
1031 &station.node_id,
1032 &station.client_node_id,
1033 &tbs,
1034 );
1035 if !verify(&message, proof, &station.identity_key, station.profile) {
1036 return Err(HandshakeError::SessionProofInvalid);
1037 }
1038 Ok(f.uint("capabilities"))
1039}
1040
1041fn refusal_before_challenge(frame: &[u8]) -> Option<HandshakeError> {
1046 let f = decode(frame, "hello", &[HELLO_EARLY_KEYS]).ok()?;
1047 (f.int("accepted") == 0).then(|| HandshakeError::Refused {
1048 code: RefusalCode::read(f.text("refusal_code")),
1049 station_ranges: Some(f.ranges()),
1050 })
1051}
1052
1053pub fn status_frame(statement: &SignedTbs) -> Vec<u8> {
1055 encode_frame("status", vec![entry("statement", statement.to_value())])
1056}
1057
1058#[derive(Debug, Clone)]
1062pub struct Peer {
1063 pub profile: Profile,
1064 pub identity_key: Vec<u8>,
1065 pub binding: SignedTbs,
1066 pub now_ms: i64,
1067}
1068
1069pub fn read_status(frame: &[u8], p: &Peer) -> Result<i64, HandshakeError> {
1075 let envelope = cbor::decode(frame)
1076 .ok()
1077 .and_then(|v| crate::frame::read_envelope(&v).ok())
1078 .filter(|e| e.frame_type == "status")
1079 .ok_or(HandshakeError::Malformed)?;
1080 read_status_frame(&envelope.frame, p)
1081}
1082
1083pub fn read_status_frame(frame: &Value, p: &Peer) -> Result<i64, HandshakeError> {
1085 let statement = crate::frame::read_control(frame)
1086 .ok()
1087 .and_then(|f| f.get("statement").map(SignedTbs::from_value))
1088 .ok_or(HandshakeError::Malformed)?
1089 .map_err(|_| HandshakeError::Malformed)?;
1090 Ok(verify_status(
1091 &statement,
1092 &p.binding,
1093 &p.identity_key,
1094 p.profile,
1095 p.now_ms,
1096 )?)
1097}
1098
1099struct Fields(std::collections::HashMap<String, Value>);
1101
1102impl Fields {
1103 fn bytes(&self, key: &str) -> &[u8] {
1104 match self.0.get(key) {
1105 Some(Value::Bytes(b)) => b,
1106 _ => &[],
1107 }
1108 }
1109
1110 fn text(&self, key: &str) -> &str {
1111 match self.0.get(key) {
1112 Some(Value::Text(t)) => t,
1113 _ => "",
1114 }
1115 }
1116
1117 fn int(&self, key: &str) -> i128 {
1118 match self.0.get(key) {
1119 Some(Value::Int(n)) => *n,
1120 _ => -1,
1121 }
1122 }
1123
1124 fn uint(&self, key: &str) -> u64 {
1125 u64::try_from(self.int(key)).unwrap_or(0)
1126 }
1127
1128 fn signed(&self, key: &str) -> SignedTbs {
1129 self.0
1130 .get(key)
1131 .and_then(|v| SignedTbs::from_value(v).ok())
1132 .unwrap_or(SignedTbs {
1133 tbs: Vec::new(),
1134 signature: Vec::new(),
1135 })
1136 }
1137
1138 fn ranges(&self) -> Ranges {
1140 let (min_version, max_version) = self
1141 .0
1142 .get("versions")
1143 .and_then(versions_of)
1144 .unwrap_or((0, 0));
1145 Ranges {
1146 min_version,
1147 max_version,
1148 profiles: self
1149 .0
1150 .get("profiles")
1151 .and_then(profiles_of)
1152 .unwrap_or_default(),
1153 }
1154 }
1155}
1156
1157fn decode(frame: &[u8], frame_type: &str, layouts: &[&[&str]]) -> Result<Fields, HandshakeError> {
1161 let Ok(Value::Map(pairs)) = cbor::decode(frame) else {
1162 return Err(HandshakeError::Malformed);
1163 };
1164 let mut fields = std::collections::HashMap::with_capacity(pairs.len());
1165 let mut non_text = 0;
1166 for (key, value) in pairs {
1167 match key {
1168 Value::Text(name) => {
1169 fields.insert(name, value);
1170 }
1171 _ => non_text += 1,
1172 }
1173 }
1174 match fields.get("version") {
1175 Some(Value::Int(v)) if *v == i128::from(VERSION) => {}
1176 Some(Value::Int(_)) => return Err(HandshakeError::UnsupportedVersion),
1177 _ => return Err(HandshakeError::Malformed),
1178 }
1179 match fields.get("frame_type") {
1180 Some(Value::Text(t)) if t == frame_type => {}
1181 Some(Value::Text(_)) => return Err(HandshakeError::UnexpectedFrame),
1182 _ => return Err(HandshakeError::Malformed),
1183 }
1184 let mut keys: Vec<&str> = fields.keys().map(String::as_str).collect();
1185 keys.sort_unstable();
1186 let has_layout = layouts.contains(&keys.as_slice());
1187 if non_text > 0 || !has_layout || !fields.iter().all(|(k, v)| field_typed(k, v)) {
1188 return Err(HandshakeError::Malformed);
1189 }
1190 Ok(Fields(fields))
1191}
1192
1193fn field_typed(key: &str, v: &Value) -> bool {
1194 match key {
1195 "version" | "frame_type" => true,
1196 "profile" => matches!(v, Value::Text(_)),
1197 "versions" => versions_of(v).is_some(),
1198 "profiles" => profiles_of(v).is_some(),
1199 "nonce" => matches!(v, Value::Bytes(b) if b.len() == NONCE_SIZE),
1200 "identity_key" | "connect_key" | "proof" | "member_endorsement" | "session_proof" => {
1201 matches!(v, Value::Bytes(_))
1202 }
1203 "tls_binding" | "tls_status" | "connect_binding" | "connect_status" | "statement" => {
1204 SignedTbs::from_value(v).is_ok()
1205 }
1206 "capabilities" => matches!(v, Value::Int(n) if *n >= 0 && *n < MAX_PROTOCOL_INT),
1207 "accepted" => matches!(v, Value::Int(0 | 1)),
1208 "refusal_code" => {
1209 matches!(v, Value::Text(t) if !t.is_empty() && t.len() <= MAX_REFUSAL_CODE_BYTES)
1210 }
1211 _ => false,
1212 }
1213}
1214
1215fn versions_of(v: &Value) -> Option<(i64, i64)> {
1217 match v {
1218 Value::List(items) => match items.as_slice() {
1219 [Value::Int(lo), Value::Int(hi)] if *lo >= 0 && *hi < MAX_PROTOCOL_INT && lo <= hi => {
1220 Some((*lo as i64, *hi as i64))
1221 }
1222 _ => None,
1223 },
1224 _ => None,
1225 }
1226}
1227
1228fn profiles_of(v: &Value) -> Option<Vec<String>> {
1230 let Value::List(items) = v else {
1231 return None;
1232 };
1233 if items.is_empty() || items.len() > MAX_PROFILES {
1234 return None;
1235 }
1236 let mut out: Vec<String> = Vec::with_capacity(items.len());
1237 for item in items {
1238 match item {
1239 Value::Text(name) if !name.is_empty() && !out.contains(name) => out.push(name.clone()),
1240 _ => return None,
1241 }
1242 }
1243 Some(out)
1244}
1245
1246fn in_leaf(key: &[u8], leaf: &[u8]) -> bool {
1248 key.len() >= MLDSA_KEY_SIZE
1249 && leaf
1250 .windows(MLDSA_KEY_SIZE)
1251 .any(|w| w == &key[..MLDSA_KEY_SIZE])
1252}
1253
1254fn shares_a_half(a: &[u8], b: &[u8]) -> bool {
1256 if a.len() < MLDSA_KEY_SIZE || b.len() < MLDSA_KEY_SIZE {
1257 return a == b;
1258 }
1259 let (classical_a, classical_b) = (&a[MLDSA_KEY_SIZE..], &b[MLDSA_KEY_SIZE..]);
1260 a[..MLDSA_KEY_SIZE] == b[..MLDSA_KEY_SIZE]
1261 || (!classical_a.is_empty() && classical_a == classical_b)
1262}
1263
1264fn encode_frame(frame_type: &str, entries: Vec<(Value, Value)>) -> Vec<u8> {
1266 let mut all = vec![
1267 entry("version", Value::Int(i128::from(VERSION))),
1268 entry("frame_type", Value::text(frame_type)),
1269 ];
1270 all.extend(entries);
1271 cbor::encode(&Value::Map(all)).expect("a handshake frame's integers are all below 2^53")
1272}
1273
1274fn entry(key: &str, value: Value) -> (Value, Value) {
1275 (Value::text(key), value)
1276}
1277
1278fn hex_of(bytes: &[u8]) -> String {
1279 bytes.iter().map(|b| format!("{b:02x}")).collect()
1280}