use std::fmt;
use std::sync::Arc;
use sha2::{Digest, Sha384};
use crate::binding::{
verify_connect_binding, verify_status, verify_tls_binding, BindingError, SignedTbs,
};
use crate::cbor::{self, Value};
use crate::node_key::{
carried_key_well_formed, node_id_of, puzzle_solved, signature_size, verify, KeyError, NodeKey,
};
use crate::profile::Profile;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod wire_1_0_tests;
pub const VERSION: i64 = 5;
pub const EXPORTER_LABEL: &str = "EXPORTER-macula-session-v1";
pub const CAP_STATION: u64 = 1 << 0;
pub const CAP_SWIM_INDIRECT: u64 = 1 << 1;
pub const CAP_RELAY_OVERLOADED: u64 = 1 << 2;
pub const MAX_CAPABILITY_BIT: u32 = 52;
const NONCE_SIZE: usize = 32;
const EXPORTER_SIZE: usize = 32;
const MAX_PROTOCOL_INT: i128 = 1 << 53;
const MLDSA_KEY_SIZE: usize = 2592;
const MAX_PROFILES: usize = 8;
const MAX_REFUSAL_CODE_BYTES: usize = 64;
const CONNECT_PROOF_LABEL: &[u8] = b"MACULA-PQ-CONNECT-PROOF-V3";
const SESSION_PROOF_LABEL: &[u8] = b"MACULA-PQ-SESSION-PROOF-V2";
pub type Exporter = dyn Fn(&str, &[u8], usize) -> Option<Vec<u8>> + Send + Sync;
pub type Floor = dyn Fn(&[u8; 32]) -> i64 + Send + Sync;
const OPENER_KEYS: &[&str] = &["frame_type", "profiles", "version", "versions"];
const CHALLENGE_KEYS: &[&str] = &[
"frame_type",
"identity_key",
"nonce",
"profile",
"profiles",
"tls_binding",
"tls_status",
"version",
"versions",
];
const CONNECT_KEYS: &[&str] = &[
"capabilities",
"connect_binding",
"connect_key",
"connect_status",
"frame_type",
"identity_key",
"member_endorsement",
"profile",
"profiles",
"proof",
"version",
"versions",
];
const HELLO_PROVED_KEYS: &[&str] = &[
"accepted",
"capabilities",
"frame_type",
"session_proof",
"version",
];
const HELLO_ACCEPTED_KEYS: &[&str] = &["accepted", "capabilities", "frame_type", "version"];
const HELLO_REFUSED_KEYS: &[&str] = &[
"accepted",
"capabilities",
"frame_type",
"refusal_code",
"version",
];
const HELLO_EARLY_KEYS: &[&str] = &[
"accepted",
"frame_type",
"profiles",
"refusal_code",
"version",
"versions",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefusalCode {
UnsupportedVersion,
ProfileUnsupported,
PuzzleInvalid,
SessionProofRate,
NotAccepted,
}
impl RefusalCode {
const ALL: [RefusalCode; 5] = [
RefusalCode::UnsupportedVersion,
RefusalCode::ProfileUnsupported,
RefusalCode::PuzzleInvalid,
RefusalCode::SessionProofRate,
RefusalCode::NotAccepted,
];
pub fn name(self) -> &'static str {
match self {
RefusalCode::UnsupportedVersion => "unsupported_version",
RefusalCode::ProfileUnsupported => "profile_unsupported",
RefusalCode::PuzzleInvalid => "puzzle_invalid",
RefusalCode::SessionProofRate => "session_proof_rate",
RefusalCode::NotAccepted => "not_accepted",
}
}
fn read(name: &str) -> RefusalCode {
RefusalCode::ALL
.into_iter()
.find(|code| code.name() == name)
.unwrap_or_else(|| {
crate::frame::count_enum_fallback("refusal_code");
RefusalCode::NotAccepted
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ranges {
pub min_version: i64,
pub max_version: i64,
pub profiles: Vec<String>,
}
impl Ranges {
pub fn supported(profile: Profile) -> Ranges {
Ranges {
min_version: VERSION,
max_version: VERSION,
profiles: vec![profile.name().to_string()],
}
}
fn entries(&self) -> Vec<(Value, Value)> {
vec![
entry(
"versions",
Value::List(vec![
Value::Int(i128::from(self.min_version)),
Value::Int(i128::from(self.max_version)),
]),
),
entry(
"profiles",
Value::List(self.profiles.iter().map(Value::text).collect()),
),
]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HandshakeError {
UnexpectedFrame,
UnsupportedVersion,
ProfileUnsupported,
DowngradeDetected,
NegotiationMismatch,
VersionBelowFloor,
Malformed,
KeyPurposeReuse,
PeerIdentityMismatch {
expected: [u8; 32],
derived: [u8; 32],
},
PuzzleInvalid,
ProofInvalid,
Refused {
code: RefusalCode,
station_ranges: Option<Ranges>,
},
InvalidStationSession,
SessionProofInvalid,
SessionProofMissing,
SessionProofRate,
ExporterUnavailable,
Binding(BindingError),
Key(KeyError),
}
impl fmt::Display for HandshakeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HandshakeError::UnexpectedFrame => f.write_str("unexpected frame"),
HandshakeError::UnsupportedVersion => f.write_str("unsupported_version"),
HandshakeError::ProfileUnsupported => f.write_str("profile_unsupported"),
HandshakeError::DowngradeDetected => f.write_str("downgrade_detected"),
HandshakeError::NegotiationMismatch => f.write_str("negotiation_mismatch"),
HandshakeError::VersionBelowFloor => f.write_str("version_below_floor"),
HandshakeError::Malformed => f.write_str("malformed frame"),
HandshakeError::KeyPurposeReuse => f.write_str("a key would serve two purposes"),
HandshakeError::PeerIdentityMismatch { expected, derived } => write!(
f,
"dialed node_id {}, but the station's key derives {}",
hex_of(expected),
hex_of(derived)
),
HandshakeError::PuzzleInvalid => f.write_str("the node_id does not meet the puzzle"),
HandshakeError::ProofInvalid => f.write_str("the CONNECT proof does not verify"),
HandshakeError::Refused { code, .. } => {
write!(f, "the station refused the connection: {}", code.name())
}
HandshakeError::InvalidStationSession => {
f.write_str("the station session has an unknown puzzle difficulty")
}
HandshakeError::SessionProofInvalid => f.write_str("the session proof does not verify"),
HandshakeError::SessionProofMissing => {
f.write_str("the HELLO carries no session proof")
}
HandshakeError::SessionProofRate => {
f.write_str("the station's session proof budget is spent")
}
HandshakeError::ExporterUnavailable => f.write_str("the TLS exporter is unavailable"),
HandshakeError::Binding(e) => write!(f, "{e}"),
HandshakeError::Key(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for HandshakeError {}
impl From<BindingError> for HandshakeError {
fn from(e: BindingError) -> Self {
HandshakeError::Binding(e)
}
}
impl HandshakeError {
fn wire_refusal(&self) -> RefusalCode {
match self {
HandshakeError::UnsupportedVersion => RefusalCode::UnsupportedVersion,
HandshakeError::PuzzleInvalid => RefusalCode::PuzzleInvalid,
HandshakeError::SessionProofRate => RefusalCode::SessionProofRate,
_ => RefusalCode::NotAccepted,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PuzzleMode {
Off,
LogOnly,
Enforce,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PuzzleResult {
Solved,
Unsolved,
NotChecked,
}
fn negotiate(client: &Ranges, station: &Ranges) -> Result<(i64, String), HandshakeError> {
let version = client.max_version.min(station.max_version).min(VERSION);
if version < client.min_version.max(station.min_version) {
return Err(HandshakeError::UnsupportedVersion);
}
client
.profiles
.iter()
.find(|p| station.profiles.contains(p))
.map(|p| (version, p.clone()))
.ok_or(HandshakeError::ProfileUnsupported)
}
pub fn opener(ranges: &Ranges) -> Vec<u8> {
encode_frame("opener", ranges.entries())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Negotiated {
pub version: i64,
pub profile: String,
pub client: Ranges,
}
pub fn read_opener(
opener: &[u8],
station: &Ranges,
) -> (Result<Negotiated, HandshakeError>, Option<Vec<u8>>) {
let read = decode(opener, "opener", &[OPENER_KEYS]).and_then(|f| {
let client = f.ranges();
let (version, profile) = negotiate(&client, station)?;
Ok(Negotiated {
version,
profile,
client,
})
});
match &read {
Err(e @ (HandshakeError::UnsupportedVersion | HandshakeError::ProfileUnsupported)) => {
let hello = hello_early_refusal(e, station);
(read, Some(hello))
}
_ => (read, None),
}
}
#[derive(Debug, Clone)]
pub struct StationMaterial {
pub profile: Profile,
pub identity_key: Vec<u8>,
pub tls_binding: SignedTbs,
pub tls_status: SignedTbs,
}
pub fn challenge(
m: &StationMaterial,
n: &Negotiated,
station: &Ranges,
) -> Result<Vec<u8>, HandshakeError> {
let mut nonce = [0u8; NONCE_SIZE];
aws_lc_rs::rand::fill(&mut nonce)
.map_err(|_| HandshakeError::Key(KeyError::RandomnessUnavailable))?;
let mut entries = station.entries();
entries.extend([
entry("nonce", Value::Bytes(nonce.to_vec())),
entry("profile", Value::text(n.profile.as_str())),
entry("identity_key", Value::Bytes(m.identity_key.clone())),
entry("tls_binding", m.tls_binding.to_value()),
entry("tls_status", m.tls_status.to_value()),
]);
Ok(encode_frame("challenge", entries))
}
pub struct ClientSession<'a> {
pub profile: Profile,
pub expected_node_id: [u8; 32],
pub leaf: &'a [u8],
pub identity_key: Vec<u8>,
pub connect_key: &'a NodeKey,
pub connect_binding: &'a SignedTbs,
pub connect_status: &'a SignedTbs,
pub capabilities: u64,
pub now_ms: i64,
pub member_endorsement: Vec<u8>,
pub ranges: Ranges,
pub floor: &'a Floor,
pub export: &'a Exporter,
}
struct ChallengeCheck<'a> {
profile: Profile,
expected_node_id: [u8; 32],
leaf: &'a [u8],
identity_key: &'a [u8],
connect_public: &'a [u8],
now_ms: i64,
ranges: &'a Ranges,
floor: &'a Floor,
export: &'a Exporter,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Station {
pub node_id: [u8; 32],
pub identity_key: Vec<u8>,
pub tls_binding: SignedTbs,
pub status_expires_at: i64,
pub binding_not_after: i64,
pub version: i64,
profile: Profile,
nonce: Vec<u8>,
leaf: Vec<u8>,
exporter_value: Vec<u8>,
challenge: Vec<u8>,
connect: Vec<u8>,
client_node_id: [u8; 32],
}
impl Station {
fn proof_message(&self, connect_tbs: &[u8]) -> Vec<u8> {
proof_message(
&self.nonce,
&self.node_id,
&self.client_node_id,
&self.leaf,
&self.challenge,
&self.exporter_value,
connect_tbs,
)
}
}
fn read_challenge(challenge: &[u8], s: &ChallengeCheck<'_>) -> Result<Station, HandshakeError> {
let f = decode(challenge, "challenge", &[CHALLENGE_KEYS])
.map_err(|e| refusal_before_challenge(challenge).unwrap_or(e))?;
let negotiated = negotiate(s.ranges, &f.ranges()).ok();
let chosen = negotiated.filter(|(version, profile)| {
i128::from(*version) == f.int("version")
&& profile == f.text("profile")
&& profile == s.profile.name()
});
let Some((version, _)) = chosen else {
return Err(HandshakeError::DowngradeDetected);
};
if version < (s.floor)(&s.expected_node_id) {
return Err(HandshakeError::VersionBelowFloor);
}
let station_key = f.bytes("identity_key");
if !carried_key_well_formed(station_key, s.profile) {
return Err(HandshakeError::Malformed);
}
if shares_a_half(s.identity_key, s.connect_public)
|| in_leaf(station_key, s.leaf)
|| in_leaf(s.connect_public, s.leaf)
{
return Err(HandshakeError::KeyPurposeReuse);
}
let station_node_id = node_id_of(station_key, s.profile);
if station_node_id != s.expected_node_id {
return Err(HandshakeError::PeerIdentityMismatch {
expected: s.expected_node_id,
derived: station_node_id,
});
}
let tls_binding = f.signed("tls_binding");
let binding = verify_tls_binding(&tls_binding, station_key, s.profile, s.leaf, s.now_ms)?;
let status = f.signed("tls_status");
let expires_at = verify_status(&status, &tls_binding, station_key, s.profile, s.now_ms)?;
let client_node_id = node_id_of(s.identity_key, s.profile);
let e = session_exported(s.export, &client_node_id, &station_node_id)?;
Ok(Station {
node_id: station_node_id,
identity_key: station_key.to_vec(),
tls_binding,
status_expires_at: expires_at,
binding_not_after: binding.not_after,
version,
profile: s.profile,
nonce: f.bytes("nonce").to_vec(),
leaf: s.leaf.to_vec(),
exporter_value: e,
challenge: challenge.to_vec(),
connect: Vec::new(),
client_node_id,
})
}
pub fn answer_challenge(
challenge: &[u8],
s: &ClientSession<'_>,
) -> Result<(Vec<u8>, Station), HandshakeError> {
let connect_public = s.connect_key.public_key();
let mut station = read_challenge(
challenge,
&ChallengeCheck {
profile: s.profile,
expected_node_id: s.expected_node_id,
leaf: s.leaf,
identity_key: &s.identity_key,
connect_public: &connect_public,
now_ms: s.now_ms,
ranges: &s.ranges,
floor: s.floor,
export: s.export,
},
)?;
let mut entries = s.ranges.entries();
entries.extend([
entry("profile", Value::text(s.profile.name())),
entry("identity_key", Value::Bytes(s.identity_key.clone())),
entry("connect_key", Value::Bytes(connect_public)),
entry("connect_binding", s.connect_binding.to_value()),
entry("connect_status", s.connect_status.to_value()),
entry("capabilities", Value::Int(i128::from(s.capabilities))),
entry(
"member_endorsement",
Value::Bytes(s.member_endorsement.clone()),
),
]);
let with_proof = |proof: Vec<u8>| {
let mut all = entries.clone();
all.push(entry("proof", Value::Bytes(proof)));
encode_frame("connect", all)
};
let tbs = with_proof(Vec::new());
let proof = s
.connect_key
.sign(&station.proof_message(&tbs))
.map_err(HandshakeError::Key)?;
station.connect = with_proof(proof);
Ok((station.connect.clone(), station))
}
fn session_exported(
export: &Exporter,
client_node_id: &[u8; 32],
station_node_id: &[u8; 32],
) -> Result<Vec<u8>, HandshakeError> {
let context = [client_node_id.as_slice(), station_node_id].concat();
match export(EXPORTER_LABEL, &context, EXPORTER_SIZE) {
Some(e) if e.len() == EXPORTER_SIZE => Ok(e),
_ => Err(HandshakeError::ExporterUnavailable),
}
}
pub type SessionProofSigner =
dyn Fn(&[u8; 32], &[u8]) -> Result<Vec<u8>, HandshakeError> + Send + Sync;
#[derive(Clone)]
pub struct StationSession {
pub profile: Profile,
pub negotiated: Negotiated,
pub challenge: Vec<u8>,
pub leaf: Vec<u8>,
pub puzzle_difficulty: u32,
pub puzzle_mode: PuzzleMode,
pub capabilities: u64,
pub now_ms: i64,
pub floor: Arc<Floor>,
pub export: Arc<Exporter>,
pub sign_session_proof: Arc<SessionProofSigner>,
}
impl fmt::Debug for StationSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StationSession")
.field("profile", &self.profile)
.field("negotiated", &self.negotiated)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Client {
pub node_id: [u8; 32],
pub identity_key: Vec<u8>,
pub connect_key: Vec<u8>,
pub connect_binding: SignedTbs,
pub capabilities: u64,
pub status_expires_at: i64,
pub binding_not_after: i64,
pub puzzle: PuzzleResult,
pub member_endorsement: Vec<u8>,
pub version: i64,
}
pub fn accept_connect(
connect: &[u8],
s: &StationSession,
) -> (Result<Client, HandshakeError>, Vec<u8>) {
match check_connect(connect, s) {
Ok((client, session_proof)) => {
let hello = hello_accepted(s.capabilities, &session_proof);
(Ok(client), hello)
}
Err(e) => {
let hello = hello_refused(e.wire_refusal());
(Err(e), hello)
}
}
}
fn check_connect(connect: &[u8], s: &StationSession) -> Result<(Client, Vec<u8>), HandshakeError> {
if s.puzzle_difficulty > 256 {
return Err(HandshakeError::InvalidStationSession);
}
let f = decode(connect, "connect", &[CONNECT_KEYS])?;
if f.ranges() != s.negotiated.client {
return Err(HandshakeError::NegotiationMismatch);
}
if f.text("profile") != s.negotiated.profile || s.negotiated.profile != s.profile.name() {
return Err(HandshakeError::ProfileUnsupported);
}
let (identity_key, connect_key, proof) = (
f.bytes("identity_key"),
f.bytes("connect_key"),
f.bytes("proof"),
);
check_carried_keys(identity_key, connect_key, proof, s)?;
let node_id = node_id_of(identity_key, s.profile);
let puzzle = puzzle_verdict(&node_id, s)?;
if s.negotiated.version < (s.floor)(&node_id) {
return Err(HandshakeError::VersionBelowFloor);
}
let (connect_binding, binding_not_after, expires_at) =
verified_connect_statements(&f, identity_key, connect_key, s)?;
let session_proof = proved_session(connect, proof, connect_key, &node_id, s)?;
Ok((
Client {
node_id,
identity_key: identity_key.to_vec(),
connect_key: connect_key.to_vec(),
connect_binding,
capabilities: f.uint("capabilities"),
status_expires_at: expires_at,
binding_not_after,
puzzle,
member_endorsement: f.bytes("member_endorsement").to_vec(),
version: s.negotiated.version,
},
session_proof,
))
}
fn proved_session(
connect: &[u8],
proof: &[u8],
connect_key: &[u8],
node_id: &[u8; 32],
s: &StationSession,
) -> Result<Vec<u8>, HandshakeError> {
let challenge = decode(&s.challenge, "challenge", &[CHALLENGE_KEYS])
.map_err(|_| HandshakeError::ProofInvalid)?;
let station_node_id = node_id_of(challenge.bytes("identity_key"), s.profile);
let e = session_exported(s.export.as_ref(), node_id, &station_node_id)?;
let tbs = frame_tbs(connect, "proof")?;
let message = proof_message(
challenge.bytes("nonce"),
&station_node_id,
node_id,
&s.leaf,
&s.challenge,
&e,
&tbs,
);
if !verify(&message, proof, connect_key, s.profile) {
return Err(HandshakeError::ProofInvalid);
}
let hello_tbs = hello_accepted(s.capabilities, &[]);
(s.sign_session_proof)(
node_id,
&session_proof_message(
&e,
&s.challenge,
connect,
&station_node_id,
node_id,
&hello_tbs,
),
)
}
fn check_carried_keys(
identity_key: &[u8],
connect_key: &[u8],
proof: &[u8],
s: &StationSession,
) -> Result<(), HandshakeError> {
if !carried_key_well_formed(identity_key, s.profile)
|| !carried_key_well_formed(connect_key, s.profile)
|| proof.len() != signature_size(s.profile)
{
return Err(HandshakeError::Malformed);
}
if shares_a_half(identity_key, connect_key) || in_leaf(connect_key, &s.leaf) {
return Err(HandshakeError::KeyPurposeReuse);
}
Ok(())
}
fn puzzle_verdict(node_id: &[u8; 32], s: &StationSession) -> Result<PuzzleResult, HandshakeError> {
let puzzle = match s.puzzle_mode {
PuzzleMode::Off => PuzzleResult::NotChecked,
_ if puzzle_solved(node_id, s.puzzle_difficulty) => PuzzleResult::Solved,
_ => PuzzleResult::Unsolved,
};
if puzzle == PuzzleResult::Unsolved && s.puzzle_mode == PuzzleMode::Enforce {
return Err(HandshakeError::PuzzleInvalid);
}
Ok(puzzle)
}
fn verified_connect_statements(
f: &Fields,
identity_key: &[u8],
connect_key: &[u8],
s: &StationSession,
) -> Result<(SignedTbs, i64, i64), HandshakeError> {
let connect_binding = f.signed("connect_binding");
let binding = verify_connect_binding(
&connect_binding,
identity_key,
s.profile,
connect_key,
s.now_ms,
)?;
let expires_at = verify_status(
&f.signed("connect_status"),
&connect_binding,
identity_key,
s.profile,
s.now_ms,
)?;
Ok((connect_binding, binding.not_after, expires_at))
}
fn frame_tbs(raw: &[u8], signature_key: &str) -> Result<Vec<u8>, HandshakeError> {
let v = cbor::decode(raw).map_err(|_| HandshakeError::Malformed)?;
if cbor::encode(&v).ok().as_deref() != Some(raw) {
return Err(HandshakeError::Malformed);
}
let rebuilt = v.with_field(signature_key, Value::Bytes(Vec::new()));
cbor::encode(&rebuilt).map_err(|_| HandshakeError::Malformed)
}
fn proof_message(
nonce: &[u8],
station_node_id: &[u8; 32],
client_node_id: &[u8; 32],
leaf: &[u8],
challenge: &[u8],
e: &[u8],
connect_tbs: &[u8],
) -> Vec<u8> {
let mut out = Vec::with_capacity(CONNECT_PROOF_LABEL.len() + 1 + NONCE_SIZE + 64 + 144 + 32);
out.extend_from_slice(CONNECT_PROOF_LABEL);
out.push(0);
out.extend_from_slice(nonce);
out.extend_from_slice(station_node_id);
out.extend_from_slice(client_node_id);
out.extend_from_slice(&Sha384::digest(leaf));
out.extend_from_slice(&Sha384::digest(challenge));
out.extend_from_slice(e);
out.extend_from_slice(&Sha384::digest(connect_tbs));
out
}
fn session_proof_message(
e: &[u8],
challenge: &[u8],
connect: &[u8],
station_node_id: &[u8; 32],
client_node_id: &[u8; 32],
hello_tbs: &[u8],
) -> Vec<u8> {
let mut out = Vec::with_capacity(SESSION_PROOF_LABEL.len() + 1 + EXPORTER_SIZE + 144 + 64);
out.extend_from_slice(SESSION_PROOF_LABEL);
out.push(0);
out.extend_from_slice(e);
out.extend_from_slice(&Sha384::digest(challenge));
out.extend_from_slice(&Sha384::digest(connect));
out.extend_from_slice(station_node_id);
out.extend_from_slice(client_node_id);
out.extend_from_slice(&Sha384::digest(hello_tbs));
out
}
fn hello_accepted(capabilities: u64, session_proof: &[u8]) -> Vec<u8> {
encode_frame(
"hello",
vec![
entry("accepted", Value::Int(1)),
entry("capabilities", Value::Int(i128::from(capabilities))),
entry("session_proof", Value::Bytes(session_proof.to_vec())),
],
)
}
fn hello_refused(code: RefusalCode) -> Vec<u8> {
encode_frame(
"hello",
vec![
entry("accepted", Value::Int(0)),
entry("refusal_code", Value::text(code.name())),
entry("capabilities", Value::Int(0)),
],
)
}
fn hello_early_refusal(e: &HandshakeError, station: &Ranges) -> Vec<u8> {
let code = match e {
HandshakeError::UnsupportedVersion => RefusalCode::UnsupportedVersion,
_ => RefusalCode::ProfileUnsupported,
};
let mut entries = station.entries();
entries.extend([
entry("accepted", Value::Int(0)),
entry("refusal_code", Value::text(code.name())),
]);
encode_frame("hello", entries)
}
pub fn read_hello(hello: &[u8], station: &Station) -> Result<u64, HandshakeError> {
let f = decode(
hello,
"hello",
&[
HELLO_PROVED_KEYS,
HELLO_ACCEPTED_KEYS,
HELLO_REFUSED_KEYS,
HELLO_EARLY_KEYS,
],
)?;
let accepted = f.int("accepted");
let code =
f.0.get("refusal_code")
.map(|_| RefusalCode::read(f.text("refusal_code")));
let early = f.0.contains_key("versions");
match (accepted, code, early) {
(0, Some(code), true) => {
return Err(HandshakeError::Refused {
code,
station_ranges: Some(f.ranges()),
})
}
(0, Some(code), false) => {
return Err(HandshakeError::Refused {
code,
station_ranges: None,
})
}
(1, None, false) => {}
_ => return Err(HandshakeError::Malformed),
}
let Some(Value::Bytes(proof)) = f.0.get("session_proof") else {
return Err(HandshakeError::SessionProofMissing);
};
if proof.len() != signature_size(station.profile) {
return Err(HandshakeError::Malformed);
}
let tbs = frame_tbs(hello, "session_proof")?;
let message = session_proof_message(
&station.exporter_value,
&station.challenge,
&station.connect,
&station.node_id,
&station.client_node_id,
&tbs,
);
if !verify(&message, proof, &station.identity_key, station.profile) {
return Err(HandshakeError::SessionProofInvalid);
}
Ok(f.uint("capabilities"))
}
fn refusal_before_challenge(frame: &[u8]) -> Option<HandshakeError> {
let f = decode(frame, "hello", &[HELLO_EARLY_KEYS]).ok()?;
(f.int("accepted") == 0).then(|| HandshakeError::Refused {
code: RefusalCode::read(f.text("refusal_code")),
station_ranges: Some(f.ranges()),
})
}
pub fn status_frame(statement: &SignedTbs) -> Vec<u8> {
encode_frame("status", vec![entry("statement", statement.to_value())])
}
#[derive(Debug, Clone)]
pub struct Peer {
pub profile: Profile,
pub identity_key: Vec<u8>,
pub binding: SignedTbs,
pub now_ms: i64,
}
pub fn read_status(frame: &[u8], p: &Peer) -> Result<i64, HandshakeError> {
let envelope = cbor::decode(frame)
.ok()
.and_then(|v| crate::frame::read_envelope(&v).ok())
.filter(|e| e.frame_type == "status")
.ok_or(HandshakeError::Malformed)?;
read_status_frame(&envelope.frame, p)
}
pub fn read_status_frame(frame: &Value, p: &Peer) -> Result<i64, HandshakeError> {
let statement = crate::frame::read_control(frame)
.ok()
.and_then(|f| f.get("statement").map(SignedTbs::from_value))
.ok_or(HandshakeError::Malformed)?
.map_err(|_| HandshakeError::Malformed)?;
Ok(verify_status(
&statement,
&p.binding,
&p.identity_key,
p.profile,
p.now_ms,
)?)
}
struct Fields(std::collections::HashMap<String, Value>);
impl Fields {
fn bytes(&self, key: &str) -> &[u8] {
match self.0.get(key) {
Some(Value::Bytes(b)) => b,
_ => &[],
}
}
fn text(&self, key: &str) -> &str {
match self.0.get(key) {
Some(Value::Text(t)) => t,
_ => "",
}
}
fn int(&self, key: &str) -> i128 {
match self.0.get(key) {
Some(Value::Int(n)) => *n,
_ => -1,
}
}
fn uint(&self, key: &str) -> u64 {
u64::try_from(self.int(key)).unwrap_or(0)
}
fn signed(&self, key: &str) -> SignedTbs {
self.0
.get(key)
.and_then(|v| SignedTbs::from_value(v).ok())
.unwrap_or(SignedTbs {
tbs: Vec::new(),
signature: Vec::new(),
})
}
fn ranges(&self) -> Ranges {
let (min_version, max_version) = self
.0
.get("versions")
.and_then(versions_of)
.unwrap_or((0, 0));
Ranges {
min_version,
max_version,
profiles: self
.0
.get("profiles")
.and_then(profiles_of)
.unwrap_or_default(),
}
}
}
fn decode(frame: &[u8], frame_type: &str, layouts: &[&[&str]]) -> Result<Fields, HandshakeError> {
let Ok(Value::Map(pairs)) = cbor::decode(frame) else {
return Err(HandshakeError::Malformed);
};
let mut fields = std::collections::HashMap::with_capacity(pairs.len());
let mut non_text = 0;
for (key, value) in pairs {
match key {
Value::Text(name) => {
fields.insert(name, value);
}
_ => non_text += 1,
}
}
match fields.get("version") {
Some(Value::Int(v)) if *v == i128::from(VERSION) => {}
Some(Value::Int(_)) => return Err(HandshakeError::UnsupportedVersion),
_ => return Err(HandshakeError::Malformed),
}
match fields.get("frame_type") {
Some(Value::Text(t)) if t == frame_type => {}
Some(Value::Text(_)) => return Err(HandshakeError::UnexpectedFrame),
_ => return Err(HandshakeError::Malformed),
}
let mut keys: Vec<&str> = fields.keys().map(String::as_str).collect();
keys.sort_unstable();
let has_layout = layouts.contains(&keys.as_slice());
if non_text > 0 || !has_layout || !fields.iter().all(|(k, v)| field_typed(k, v)) {
return Err(HandshakeError::Malformed);
}
Ok(Fields(fields))
}
fn field_typed(key: &str, v: &Value) -> bool {
match key {
"version" | "frame_type" => true,
"profile" => matches!(v, Value::Text(_)),
"versions" => versions_of(v).is_some(),
"profiles" => profiles_of(v).is_some(),
"nonce" => matches!(v, Value::Bytes(b) if b.len() == NONCE_SIZE),
"identity_key" | "connect_key" | "proof" | "member_endorsement" | "session_proof" => {
matches!(v, Value::Bytes(_))
}
"tls_binding" | "tls_status" | "connect_binding" | "connect_status" | "statement" => {
SignedTbs::from_value(v).is_ok()
}
"capabilities" => matches!(v, Value::Int(n) if *n >= 0 && *n < MAX_PROTOCOL_INT),
"accepted" => matches!(v, Value::Int(0 | 1)),
"refusal_code" => {
matches!(v, Value::Text(t) if !t.is_empty() && t.len() <= MAX_REFUSAL_CODE_BYTES)
}
_ => false,
}
}
fn versions_of(v: &Value) -> Option<(i64, i64)> {
match v {
Value::List(items) => match items.as_slice() {
[Value::Int(lo), Value::Int(hi)] if *lo >= 0 && *hi < MAX_PROTOCOL_INT && lo <= hi => {
Some((*lo as i64, *hi as i64))
}
_ => None,
},
_ => None,
}
}
fn profiles_of(v: &Value) -> Option<Vec<String>> {
let Value::List(items) = v else {
return None;
};
if items.is_empty() || items.len() > MAX_PROFILES {
return None;
}
let mut out: Vec<String> = Vec::with_capacity(items.len());
for item in items {
match item {
Value::Text(name) if !name.is_empty() && !out.contains(name) => out.push(name.clone()),
_ => return None,
}
}
Some(out)
}
fn in_leaf(key: &[u8], leaf: &[u8]) -> bool {
key.len() >= MLDSA_KEY_SIZE
&& leaf
.windows(MLDSA_KEY_SIZE)
.any(|w| w == &key[..MLDSA_KEY_SIZE])
}
fn shares_a_half(a: &[u8], b: &[u8]) -> bool {
if a.len() < MLDSA_KEY_SIZE || b.len() < MLDSA_KEY_SIZE {
return a == b;
}
let (classical_a, classical_b) = (&a[MLDSA_KEY_SIZE..], &b[MLDSA_KEY_SIZE..]);
a[..MLDSA_KEY_SIZE] == b[..MLDSA_KEY_SIZE]
|| (!classical_a.is_empty() && classical_a == classical_b)
}
fn encode_frame(frame_type: &str, entries: Vec<(Value, Value)>) -> Vec<u8> {
let mut all = vec![
entry("version", Value::Int(i128::from(VERSION))),
entry("frame_type", Value::text(frame_type)),
];
all.extend(entries);
cbor::encode(&Value::Map(all)).expect("a handshake frame's integers are all below 2^53")
}
fn entry(key: &str, value: Value) -> (Value, Value) {
(Value::text(key), value)
}
fn hex_of(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}