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 connect = signed_connect(
378 s,
379 f.bytes("nonce"),
380 &station_node_id,
381 &client_node_id,
382 challenge,
383 &e,
384 connect_key,
385 )?;
386 Ok((
387 connect.clone(),
388 Station {
389 node_id: station_node_id,
390 identity_key: station_key.to_vec(),
391 tls_binding,
392 status_expires_at: expires_at,
393 binding_not_after: binding.not_after,
394 version: s.version,
395 profile: s.profile,
396 exporter_value: e,
397 challenge: challenge.to_vec(),
398 connect,
399 client_node_id,
400 },
401 ))
402}
403
404fn signed_connect(
409 s: &ClientSession<'_>,
410 nonce: &[u8],
411 station_node_id: &[u8; 32],
412 client_node_id: &[u8; 32],
413 challenge: &[u8],
414 e: &[u8],
415 connect_key: Vec<u8>,
416) -> Result<Vec<u8>, HandshakeError> {
417 let proof = s
418 .connect_key
419 .sign(&proof_message(
420 s.version,
421 nonce,
422 station_node_id,
423 client_node_id,
424 s.leaf,
425 challenge,
426 e,
427 s.capabilities,
428 ))
429 .map_err(HandshakeError::Key)?;
430 Ok(encode_frame_version(
431 s.version,
432 "connect",
433 vec![
434 entry("identity_key", Value::Bytes(s.identity_key.clone())),
435 entry("connect_key", Value::Bytes(connect_key)),
436 entry("connect_binding", s.connect_binding.to_value()),
437 entry("connect_status", s.connect_status.to_value()),
438 entry("proof", Value::Bytes(proof)),
439 entry("capabilities", Value::Int(i128::from(s.capabilities))),
440 entry(
441 "member_endorsement",
442 Value::Bytes(s.member_endorsement.clone()),
443 ),
444 ],
445 ))
446}
447
448fn session_exported(
451 version: i64,
452 export: Option<&Exporter>,
453 client_node_id: &[u8; 32],
454 station_node_id: &[u8; 32],
455) -> Result<Vec<u8>, HandshakeError> {
456 match (version, export) {
457 (VERSION, _) => Ok(Vec::new()),
458 (VERSION_5, Some(export)) => {
459 let context = [client_node_id.as_slice(), station_node_id].concat();
460 match export(EXPORTER_LABEL, &context, EXPORTER_SIZE) {
461 Some(e) if e.len() == EXPORTER_SIZE => Ok(e),
462 _ => Err(HandshakeError::ExporterUnavailable),
463 }
464 }
465 (VERSION_5, None) => Err(HandshakeError::ExporterUnavailable),
466 _ => Err(HandshakeError::UnsupportedVersion),
467 }
468}
469
470#[derive(Debug, Clone)]
475pub struct StationSession {
476 pub profile: Profile,
477 pub challenge: Vec<u8>,
478 pub leaf: Vec<u8>,
479 pub puzzle_difficulty: u32,
480 pub puzzle_mode: PuzzleMode,
481 pub capabilities: u64,
482 pub now_ms: i64,
483 pub v5: Option<StationV5>,
484}
485
486pub type SessionProofSigner =
490 dyn Fn(&[u8; 32], &[u8]) -> Result<Vec<u8>, HandshakeError> + Send + Sync;
491
492#[derive(Clone)]
495pub struct StationV5 {
496 pub export: Arc<Exporter>,
497 pub sign_session_proof: Arc<SessionProofSigner>,
498}
499
500impl fmt::Debug for StationV5 {
501 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502 f.write_str("StationV5")
503 }
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct Client {
509 pub node_id: [u8; 32],
510 pub identity_key: Vec<u8>,
511 pub connect_key: Vec<u8>,
512 pub connect_binding: SignedTbs,
513 pub capabilities: u64,
514 pub status_expires_at: i64,
515 pub binding_not_after: i64,
516 pub puzzle: PuzzleResult,
517 pub member_endorsement: Vec<u8>,
520 pub version: i64,
522}
523
524pub fn accept_connect(
537 connect: &[u8],
538 s: &StationSession,
539) -> (Result<Client, HandshakeError>, Vec<u8>) {
540 let mut version = VERSION;
541 match check_connect(connect, s, &mut version) {
542 Ok((client, session_proof)) => {
543 let hello = hello(client.version, None, s.capabilities, session_proof);
544 (Ok(client), hello)
545 }
546 Err(e) => {
547 let code = match e {
548 HandshakeError::UnsupportedVersion => RefusalCode::UnsupportedVersion,
549 HandshakeError::PuzzleInvalid => RefusalCode::PuzzleInvalid,
550 HandshakeError::SessionProofRate => RefusalCode::SessionProofRate,
551 _ => RefusalCode::NotAccepted,
552 };
553 (Err(e), hello(version, Some(code), s.capabilities, None))
554 }
555 }
556}
557
558fn check_connect(
561 connect: &[u8],
562 s: &StationSession,
563 version: &mut i64,
564) -> Result<(Client, Option<Vec<u8>>), HandshakeError> {
565 if s.puzzle_difficulty > 256 {
566 return Err(HandshakeError::InvalidStationSession);
567 }
568 let versions: &[i64] = match s.v5 {
569 Some(_) => &[VERSION, VERSION_5],
570 None => &[VERSION],
571 };
572 let f = decode_versioned(connect, "connect", versions, &[CONNECT_KEYS])?;
573 *version = f.version();
574 let (identity_key, connect_key, proof) = (
575 f.bytes("identity_key"),
576 f.bytes("connect_key"),
577 f.bytes("proof"),
578 );
579 check_carried_keys(identity_key, connect_key, proof, s)?;
580 let node_id = node_id_of(identity_key, s.profile);
581 let puzzle = puzzle_verdict(&node_id, s)?;
582 let (connect_binding, binding_not_after, expires_at) =
583 verified_connect_statements(&f, identity_key, connect_key, s)?;
584 let challenge = decode(&s.challenge, "challenge", &[CHALLENGE_KEYS])
586 .map_err(|_| HandshakeError::ProofInvalid)?;
587 let station_node_id = node_id_of(challenge.bytes("identity_key"), s.profile);
588 let export = s.v5.as_ref().map(|v5| v5.export.as_ref());
589 let e = session_exported(*version, export, &node_id, &station_node_id)?;
590 let capabilities = f.uint("capabilities");
591 let message = proof_message(
592 *version,
593 challenge.bytes("nonce"),
594 &station_node_id,
595 &node_id,
596 &s.leaf,
597 &s.challenge,
598 &e,
599 capabilities,
600 );
601 if !verify(&message, proof, connect_key, s.profile) {
602 return Err(HandshakeError::ProofInvalid);
603 }
604 let session_proof =
605 station_session_proof(s, *version, &e, connect, &station_node_id, &node_id)?;
606 Ok((
607 Client {
608 node_id,
609 identity_key: identity_key.to_vec(),
610 connect_key: connect_key.to_vec(),
611 connect_binding,
612 capabilities,
613 status_expires_at: expires_at,
614 binding_not_after,
615 puzzle,
616 member_endorsement: f.bytes("member_endorsement").to_vec(),
617 version: *version,
618 },
619 session_proof,
620 ))
621}
622
623fn check_carried_keys(
626 identity_key: &[u8],
627 connect_key: &[u8],
628 proof: &[u8],
629 s: &StationSession,
630) -> Result<(), HandshakeError> {
631 if !carried_key_well_formed(identity_key, s.profile)
632 || !carried_key_well_formed(connect_key, s.profile)
633 || proof.len() != signature_size(s.profile)
634 {
635 return Err(HandshakeError::Malformed);
636 }
637 if shares_a_half(identity_key, connect_key) || in_leaf(connect_key, &s.leaf) {
638 return Err(HandshakeError::KeyPurposeReuse);
639 }
640 Ok(())
641}
642
643fn puzzle_verdict(node_id: &[u8; 32], s: &StationSession) -> Result<PuzzleResult, HandshakeError> {
646 let puzzle = match s.puzzle_mode {
647 PuzzleMode::Off => PuzzleResult::NotChecked,
648 _ if puzzle_solved(node_id, s.puzzle_difficulty) => PuzzleResult::Solved,
649 _ => PuzzleResult::Unsolved,
650 };
651 if puzzle == PuzzleResult::Unsolved && s.puzzle_mode == PuzzleMode::Enforce {
652 return Err(HandshakeError::PuzzleInvalid);
653 }
654 Ok(puzzle)
655}
656
657fn verified_connect_statements(
660 f: &Fields,
661 identity_key: &[u8],
662 connect_key: &[u8],
663 s: &StationSession,
664) -> Result<(SignedTbs, i64, i64), HandshakeError> {
665 let connect_binding = f.signed("connect_binding");
666 let binding = verify_connect_binding(
667 &connect_binding,
668 identity_key,
669 s.profile,
670 connect_key,
671 s.now_ms,
672 )?;
673 let expires_at = verify_status(
674 &f.signed("connect_status"),
675 &connect_binding,
676 identity_key,
677 s.profile,
678 s.now_ms,
679 )?;
680 Ok((connect_binding, binding.not_after, expires_at))
681}
682
683fn station_session_proof(
686 s: &StationSession,
687 version: i64,
688 e: &[u8],
689 connect: &[u8],
690 station_node_id: &[u8; 32],
691 node_id: &[u8; 32],
692) -> Result<Option<Vec<u8>>, HandshakeError> {
693 let session_proof = match (&s.v5, version) {
694 (Some(v5), VERSION_5) => Some((v5.sign_session_proof)(
695 node_id,
696 &session_proof_message(
697 e,
698 &s.challenge,
699 connect,
700 station_node_id,
701 node_id,
702 s.capabilities,
703 ),
704 )?),
705 _ => None,
706 };
707 Ok(session_proof)
708}
709
710#[allow(clippy::too_many_arguments)]
716fn proof_message(
717 version: i64,
718 nonce: &[u8],
719 station_node_id: &[u8; 32],
720 client_node_id: &[u8; 32],
721 leaf: &[u8],
722 challenge: &[u8],
723 e: &[u8],
724 capabilities: u64,
725) -> Vec<u8> {
726 let label = match version {
727 VERSION_5 => CONNECT_PROOF_LABEL_V2,
728 _ => CONNECT_PROOF_LABEL,
729 };
730 let mut out = Vec::with_capacity(label.len() + 1 + nonce.len() + 64 + 96 + EXPORTER_SIZE + 8);
731 out.extend_from_slice(label);
732 out.push(0);
733 out.extend_from_slice(nonce);
734 out.extend_from_slice(station_node_id);
735 out.extend_from_slice(client_node_id);
736 out.extend_from_slice(&Sha384::digest(leaf));
737 out.extend_from_slice(&Sha384::digest(challenge));
738 if version == VERSION_5 {
739 out.extend_from_slice(e);
740 out.extend_from_slice(&capabilities.to_be_bytes());
741 }
742 out
743}
744
745fn session_proof_message(
750 e: &[u8],
751 challenge: &[u8],
752 connect: &[u8],
753 station_node_id: &[u8; 32],
754 client_node_id: &[u8; 32],
755 capabilities: u64,
756) -> Vec<u8> {
757 let mut out = Vec::with_capacity(SESSION_PROOF_LABEL.len() + 1 + EXPORTER_SIZE + 96 + 64 + 8);
758 out.extend_from_slice(SESSION_PROOF_LABEL);
759 out.push(0);
760 out.extend_from_slice(e);
761 out.extend_from_slice(&Sha384::digest(challenge));
762 out.extend_from_slice(&Sha384::digest(connect));
763 out.extend_from_slice(station_node_id);
764 out.extend_from_slice(client_node_id);
765 out.extend_from_slice(&capabilities.to_be_bytes());
766 out
767}
768
769fn hello(
770 version: i64,
771 refusal: Option<RefusalCode>,
772 capabilities: u64,
773 session_proof: Option<Vec<u8>>,
774) -> Vec<u8> {
775 let mut entries = vec![
776 entry("accepted", Value::Int(i128::from(refusal.is_none()))),
777 entry("capabilities", Value::Int(i128::from(capabilities))),
778 ];
779 if let Some(code) = refusal {
780 entries.push(entry("refusal_code", Value::text(code.name())));
781 }
782 if let Some(proof) = session_proof {
783 entries.push(entry("session_proof", Value::Bytes(proof)));
784 }
785 encode_frame_version(version, "hello", entries)
786}
787
788pub fn read_hello(frame: &[u8], station: &Station) -> Result<u64, HandshakeError> {
796 let (versions, layouts): (&[i64], &[&[&str]]) = match station.version {
797 VERSION_5 => (
798 &[VERSION, VERSION_5],
799 &[HELLO_PROVED_KEYS, HELLO_ACCEPTED_KEYS, HELLO_REFUSED_KEYS],
800 ),
801 _ => (&[VERSION], &[HELLO_ACCEPTED_KEYS, HELLO_REFUSED_KEYS]),
802 };
803 let f = decode_versioned(frame, "hello", versions, layouts)?;
804 let refusal = f.0.get("refusal_code");
805 let proof = f.0.get("session_proof");
806 match (f.int("accepted"), refusal) {
807 (0, Some(Value::Text(code))) => {
808 return Err(HandshakeError::Refused(
809 RefusalCode::parse(code).ok_or(HandshakeError::Malformed)?,
810 ))
811 }
812 (1, None) => {}
813 _ => return Err(HandshakeError::Malformed),
814 }
815 let capabilities = f.uint("capabilities");
816 match (f.version(), station.version, proof) {
817 (VERSION, VERSION_5, _) => Err(HandshakeError::V4HelloToV5Connect),
818 (VERSION, _, None) => Ok(capabilities),
819 (VERSION_5, VERSION_5, None) => Err(HandshakeError::SessionProofMissing),
820 (VERSION_5, VERSION_5, Some(Value::Bytes(proof))) => {
821 if proof.len() != signature_size(station.profile) {
822 return Err(HandshakeError::Malformed);
823 }
824 let message = session_proof_message(
825 &station.exporter_value,
826 &station.challenge,
827 &station.connect,
828 &station.node_id,
829 &station.client_node_id,
830 capabilities,
831 );
832 if !verify(&message, proof, &station.identity_key, station.profile) {
833 return Err(HandshakeError::SessionProofInvalid);
834 }
835 Ok(capabilities)
836 }
837 _ => Err(HandshakeError::Malformed),
838 }
839}
840
841pub fn status_frame(statement: &SignedTbs) -> Vec<u8> {
843 encode_frame("status", vec![entry("statement", statement.to_value())])
844}
845
846#[derive(Debug, Clone)]
850pub struct Peer {
851 pub profile: Profile,
852 pub identity_key: Vec<u8>,
853 pub binding: SignedTbs,
854 pub now_ms: i64,
855}
856
857pub fn read_status(frame: &[u8], p: &Peer) -> Result<i64, HandshakeError> {
859 let f = decode(frame, "status", &[STATUS_KEYS])?;
860 Ok(verify_status(
861 &f.signed("statement"),
862 &p.binding,
863 &p.identity_key,
864 p.profile,
865 p.now_ms,
866 )?)
867}
868
869struct Fields(std::collections::HashMap<String, Value>);
871
872impl Fields {
873 fn bytes(&self, key: &str) -> &[u8] {
874 match self.0.get(key) {
875 Some(Value::Bytes(b)) => b,
876 _ => &[],
877 }
878 }
879
880 fn text(&self, key: &str) -> &str {
881 match self.0.get(key) {
882 Some(Value::Text(t)) => t,
883 _ => "",
884 }
885 }
886
887 fn int(&self, key: &str) -> i128 {
888 match self.0.get(key) {
889 Some(Value::Int(n)) => *n,
890 _ => -1,
891 }
892 }
893
894 fn uint(&self, key: &str) -> u64 {
895 u64::try_from(self.int(key)).unwrap_or(0)
896 }
897
898 fn version(&self) -> i64 {
899 i64::try_from(self.int("version")).unwrap_or(-1)
900 }
901
902 fn signed(&self, key: &str) -> SignedTbs {
903 self.0
904 .get(key)
905 .and_then(|v| SignedTbs::from_value(v).ok())
906 .unwrap_or(SignedTbs {
907 tbs: Vec::new(),
908 signature: Vec::new(),
909 })
910 }
911}
912
913fn decode(frame: &[u8], frame_type: &str, layouts: &[&[&str]]) -> Result<Fields, HandshakeError> {
917 decode_versioned(frame, frame_type, &[VERSION], layouts)
918}
919
920fn decode_versioned(
923 frame: &[u8],
924 frame_type: &str,
925 versions: &[i64],
926 layouts: &[&[&str]],
927) -> Result<Fields, HandshakeError> {
928 let Ok(Value::Map(pairs)) = cbor::decode(frame) else {
929 return Err(HandshakeError::Malformed);
930 };
931 let mut fields = std::collections::HashMap::with_capacity(pairs.len());
932 let mut non_text = 0;
933 for (key, value) in pairs {
934 match key {
935 Value::Text(name) => {
936 fields.insert(name, value);
937 }
938 _ => non_text += 1,
939 }
940 }
941 match fields.get("version") {
942 Some(Value::Int(v)) if versions.iter().any(|known| *v == i128::from(*known)) => {}
943 Some(Value::Int(_)) => return Err(HandshakeError::UnsupportedVersion),
944 _ => return Err(HandshakeError::Malformed),
945 }
946 match fields.get("frame_type") {
947 Some(Value::Text(t)) if t == frame_type => {}
948 Some(Value::Text(_)) => return Err(HandshakeError::UnexpectedFrame),
949 _ => return Err(HandshakeError::Malformed),
950 }
951 let mut keys: Vec<&str> = fields.keys().map(String::as_str).collect();
952 keys.sort_unstable();
953 let has_layout = layouts.contains(&keys.as_slice());
954 if non_text > 0 || !has_layout || !fields.iter().all(|(k, v)| field_typed(k, v)) {
955 return Err(HandshakeError::Malformed);
956 }
957 Ok(Fields(fields))
958}
959
960fn field_typed(key: &str, v: &Value) -> bool {
961 match key {
962 "version" | "frame_type" => true,
963 "profile" => matches!(v, Value::Text(_)),
964 "nonce" => matches!(v, Value::Bytes(b) if b.len() == NONCE_SIZE),
965 "identity_key" | "connect_key" | "proof" | "member_endorsement" | "session_proof" => {
966 matches!(v, Value::Bytes(_))
967 }
968 "tls_binding" | "tls_status" | "connect_binding" | "connect_status" | "statement" => {
969 SignedTbs::from_value(v).is_ok()
970 }
971 "capabilities" => {
972 matches!(v, Value::Int(n) if *n >= 0 && *n < i128::from(MAX_PROTOCOL_INT))
973 }
974 "accepted" => matches!(v, Value::Int(0 | 1)),
975 "refusal_code" => matches!(v, Value::Text(t) if RefusalCode::parse(t).is_some()),
976 _ => false,
977 }
978}
979
980fn in_leaf(key: &[u8], leaf: &[u8]) -> bool {
982 key.len() >= MLDSA_KEY_SIZE
983 && leaf
984 .windows(MLDSA_KEY_SIZE)
985 .any(|w| w == &key[..MLDSA_KEY_SIZE])
986}
987
988fn shares_a_half(a: &[u8], b: &[u8]) -> bool {
990 if a.len() < MLDSA_KEY_SIZE || b.len() < MLDSA_KEY_SIZE {
991 return a == b;
992 }
993 let (classical_a, classical_b) = (&a[MLDSA_KEY_SIZE..], &b[MLDSA_KEY_SIZE..]);
994 a[..MLDSA_KEY_SIZE] == b[..MLDSA_KEY_SIZE]
995 || (!classical_a.is_empty() && classical_a == classical_b)
996}
997
998fn encode_frame(frame_type: &str, entries: Vec<(Value, Value)>) -> Vec<u8> {
999 encode_frame_version(VERSION, frame_type, entries)
1000}
1001
1002fn encode_frame_version(version: i64, frame_type: &str, entries: Vec<(Value, Value)>) -> Vec<u8> {
1005 let mut all = vec![
1006 entry("version", Value::Int(i128::from(version))),
1007 entry("frame_type", Value::text(frame_type)),
1008 ];
1009 all.extend(entries);
1010 cbor::encode(&Value::Map(all)).expect("a handshake frame's integers are all below 2^53")
1011}
1012
1013fn entry(key: &str, value: Value) -> (Value, Value) {
1014 (Value::text(key), value)
1015}
1016
1017fn hex_of(bytes: &[u8]) -> String {
1018 bytes.iter().map(|b| format!("{b:02x}")).collect()
1019}