Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions openscreen-application/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use heapless::{String, Vec};
use openscreen_common::{MessageError, StreamId, MAX_CBOR_SIZE};
use openscreen_crypto::{CryptoRequest, CryptoResult};
use openscreen_network::{
NetworkError, NetworkEvent, NetworkInput, NetworkOutput, Spake2StateMachine,
NetworkError, NetworkEvent, NetworkInput, NetworkOutput, NetworkStateMachine,
};

pub use messages::*;
Expand Down Expand Up @@ -202,7 +202,7 @@ impl From<MessageError> for ApplicationError {

/// Application protocol state machine
pub struct ApplicationStateMachine {
network: Spake2StateMachine,
network: NetworkStateMachine,
app_state: ApplicationState,
next_request_id: u64,
}
Expand All @@ -211,7 +211,7 @@ impl ApplicationStateMachine {
/// Create a new application state machine
pub fn new() -> Self {
Self {
network: Spake2StateMachine::default(),
network: NetworkStateMachine::default(),
app_state: ApplicationState::Idle,
next_request_id: 1,
}
Expand Down
12 changes: 12 additions & 0 deletions openscreen-crypto-rustcrypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ impl RustCryptoCryptoProvider {
.unwrap_or_else(|| Identity::new(b"initiator"));
let id_b = Identity::new(b"responder");

trace!(
"SPAKE2 Start: is_responder={}, id_a={:02x?}, id_b={:02x?}, password_len={}",
is_responder,
if password_id.is_some() {
password_id.unwrap()
} else {
b"initiator".as_slice()
},
b"responder".as_slice(),
password.len()
);

// Create SPAKE2 instance - use start_a() for initiator or start_b() for responder
// SPAKE2 requires different group generators (M and N) for each role
let (spake2, outbound_msg) = if *is_responder {
Expand Down
4 changes: 3 additions & 1 deletion openscreen-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ use openscreen_crypto::{CryptoRequest, CryptoResult};
pub use crypto_data::*;
pub use messages::*;
pub use state::*;
pub use state_machine::{Spake2StateMachine, State as Spake2State};
pub use state_machine::{
ConnectionPhase, NetworkStateMachine, PeerAgentInfo, Spake2StateMachine, State as Spake2State,
};

/// Maximum encoded message size
pub const MAX_CBOR_SIZE: usize = 1024;
Expand Down
19 changes: 18 additions & 1 deletion openscreen-network/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn encode_type_key<const N: usize>(

/// Decode a type key as RFC 9000 variable-length integer from a byte slice.
/// Returns the type key and the number of bytes consumed.
fn decode_type_key(data: &[u8]) -> Result<(u16, usize), MessageError> {
pub fn decode_type_key(data: &[u8]) -> Result<(u16, usize), MessageError> {
let mut cursor = data;
let varint = VarInt::decode(&mut cursor).map_err(|_| MessageError::DecodeFailed)?;
let consumed = data.len() - cursor.remaining();
Expand Down Expand Up @@ -117,6 +117,23 @@ impl NetworkMessageType {
}
}

/// Returns true if the type key is a SPAKE2 authentication message (1001-1005).
pub fn is_auth_type_key(type_key: u16) -> bool {
NetworkMessageType::from_u16(type_key).is_ok()
}

/// Returns true if the type key is an agent-info message that may arrive
/// before authentication completes.
///
/// Per spec (application.bs §4.1): "Any agent may send [agent-info-request]
/// at any time." These messages must be tolerated during the auth phase.
///
/// Type keys: 10 (agent-info-request), 11 (agent-info-response),
/// 12 (agent-status-request), 13 (agent-status-response), 120 (agent-info-event).
pub fn is_agent_info_type_key(type_key: u16) -> bool {
matches!(type_key, 10 | 11 | 12 | 13 | 120)
}

/// SPAKE2 PSK status value
/// CDDL: spake2-psk-status = &(psk-needs-presentation: 0, psk-shown: 1, psk-input: 2)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
7 changes: 7 additions & 0 deletions openscreen-network/src/state_machine/awaiting_capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,16 @@ impl AwaitingCapabilities {

if crypto_data.is_responder {
// Responder: wait for initiator's handshake
log::debug!(
"AwaitingCapabilities: responder, transitioning to AwaitingHandshake"
);
Ok(State::AwaitingHandshake(AwaitingHandshake::new()))
} else {
// Initiator: request crypto to generate handshake
log::debug!(
"AwaitingCapabilities: initiator, requesting SPAKE2 Start with password_id=None (identity will be 'initiator'/'responder'), psk_len={}",
crypto_data.psk.len()
);
let op = CryptoOpKind::Spake2(Spake2Operation::Start {
password_id: None,
password: &crypto_data.psk,
Expand Down
4 changes: 4 additions & 0 deletions openscreen-network/src/state_machine/awaiting_handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ impl AwaitingHandshake {
// Responder doesn't have its own handshake yet -> request Spake2::Start
if crypto_data.is_responder && crypto_data.spake2_public.is_empty() {
// Responder flow: Request Spake2::Start to generate our handshake
log::debug!(
"AwaitingHandshake: responder requesting SPAKE2 Start with password_id=None (identity will be 'initiator'/'responder'), psk_len={}",
crypto_data.psk.len()
);
let op_id = 1; // TODO: proper op_id generation
let request = CryptoRequest {
op_id,
Expand Down
158 changes: 158 additions & 0 deletions openscreen-network/src/state_machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub use idle::Idle;
pub use initiating_handshake::InitiatingHandshake;
pub use negotiating::Negotiating;

use crate::messages::{decode_type_key, is_agent_info_type_key, is_auth_type_key};
use crate::{NetworkError, NetworkInput, NetworkOutput};
use heapless::Vec;

Expand Down Expand Up @@ -263,6 +264,163 @@ impl Default for Spake2StateMachine {
}
}

/// Overall network connection phase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionPhase {
/// Connected but not yet authenticated.
Unauthenticated,
/// Authentication completed successfully.
Authenticated,
}

/// Stored agent info received during the network phase.
/// `verified` is false until authentication succeeds.
#[derive(Debug, Clone, Default)]
pub struct PeerAgentInfo {
pub display_name: heapless::String<256>,
pub model_name: heapless::String<256>,
pub capabilities: heapless::Vec<u64, 16>,
pub verified: bool,
}

/// Top-level network protocol state machine.
///
/// Routes incoming data to the appropriate sub-handler based on
/// message type key:
/// - Auth messages (1001-1005) → `Spake2StateMachine`
/// - Agent-info messages (10, 11, 12, 13, 120) → logged and skipped
/// - Unknown → logged and skipped
///
/// Per spec (network.bs §6.2): "Prior to authentication, a message may be
/// exchanged (such as further metadata), but such info should be treated as
/// unverified."
pub struct NetworkStateMachine {
auth: Spake2StateMachine,
phase: ConnectionPhase,
peer_agent_info: Option<PeerAgentInfo>,
}

impl NetworkStateMachine {
/// Create a new network state machine wrapping a SPAKE2 authenticator.
pub fn new(crypto_data: crate::CryptoData) -> Self {
Self {
auth: Spake2StateMachine::new(crypto_data),
phase: ConnectionPhase::Unauthenticated,
peer_agent_info: None,
}
}

/// Handle a network input event.
///
/// Same signature as `Spake2StateMachine::handle()` so Quinn code
/// changes minimally.
pub fn handle<'a, 'b>(
&'a mut self,
input: &'b NetworkInput<'b>,
outputs: &mut Vec<NetworkOutput<'a>, 16>,
) -> Result<(), NetworkError> {
match input {
NetworkInput::DataReceived(_stream_id, data) => {
// Peek type key to route the message
let (type_key, _) =
decode_type_key(data).map_err(|_| NetworkError::DecodeFailed)?;

if is_auth_type_key(type_key) {
// Delegate to SPAKE2 state machine
self.auth.handle(input, outputs)
} else if is_agent_info_type_key(type_key) {
// Handle agent-info at network level (log and skip)
self.handle_agent_info(type_key)
} else {
// Unknown non-auth message — skip gracefully
log::debug!(
"Skipping unknown message type key {type_key} during network phase"
);
Ok(())
}
}
// All other inputs (TransportConnected, CryptoCompleted, Tick, etc.) → auth
_ => self.auth.handle(input, outputs),
}
}

fn handle_agent_info(&mut self, type_key: u16) -> Result<(), NetworkError> {
match type_key {
10 => {
// agent-info-request: peer wants our info
log::debug!("Received agent-info-request during network phase, noted");
Ok(())
}
11 => {
// agent-info-response: peer sent their info
log::debug!(
"Received agent-info-response during network phase, storing as unverified"
);
// TODO: parse AgentInfoResponse, populate self.peer_agent_info

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The handle_agent_info method contains a TODO to parse AgentInfoResponse and populate self.peer_agent_info. This indicates that the handling of agent-info responses is currently incomplete. While the PR prevents crashes, fully processing these messages is essential for the intended functionality.

Ok(())
}
12 => {
// agent-status-request
log::debug!("Received agent-status-request during network phase, noted");
Ok(())
}
13 => {
// agent-status-response
log::debug!("Received agent-status-response during network phase, noted");
Ok(())
}
120 => {
// agent-info-event
log::debug!("Received agent-info-event during network phase, noted");
Ok(())
}
_ => Ok(()),
}
}

/// Check if authentication succeeded.
pub fn is_authenticated(&self) -> bool {
self.auth.is_authenticated()
}

/// Check if authentication failed.
pub fn is_failed(&self) -> bool {
self.auth.is_failed()
}

/// Get the current connection phase, synchronising with the auth state.
pub fn phase(&mut self) -> ConnectionPhase {
if self.auth.is_authenticated() && self.phase == ConnectionPhase::Unauthenticated {
self.phase = ConnectionPhase::Authenticated;
if let Some(ref mut info) = self.peer_agent_info {
info.verified = true;
}
}
self.phase
}

/// Get peer agent info (if received).
pub fn peer_agent_info(&self) -> Option<&PeerAgentInfo> {
self.peer_agent_info.as_ref()
}

/// Get mutable access to crypto data (for setting PSK, auth token, etc.)
pub fn crypto_data_mut(&mut self) -> &mut crate::CryptoData {
self.auth.crypto_data_mut()
}

/// Get the current SPAKE2 state (for debugging/testing).
pub fn auth_state(&self) -> &State {
self.auth.state()
}
}

impl Default for NetworkStateMachine {
fn default() -> Self {
Self::new(crate::CryptoData::new())
}
}

/// Helper function to get state name for logging
fn state_name(state: &State) -> &'static str {
match state {
Expand Down
6 changes: 3 additions & 3 deletions openscreen-quinn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub mod server;

use openscreen_crypto::CryptoProvider;
use openscreen_network::{
state_machine::Spake2StateMachine, CryptoData, NetworkError, NetworkInput, NetworkOutput,
state_machine::NetworkStateMachine, CryptoData, NetworkError, NetworkInput, NetworkOutput,
};
use quinn::{ClientConfig, Endpoint};
use rustls::pki_types::CertificateDer;
Expand Down Expand Up @@ -108,7 +108,7 @@ pub struct QuinnClient<C: CryptoProvider> {
/// Active QUIC connection (if connected)
connection: Option<quinn::Connection>,
/// Network protocol state machine (owns CryptoData internally)
network_state: Spake2StateMachine,
network_state: NetworkStateMachine,
/// Crypto provider for executing crypto operations
crypto_provider: C,
/// Our TLS certificate (for computing fingerprint)
Expand Down Expand Up @@ -171,7 +171,7 @@ impl<C: CryptoProvider> QuinnClient<C> {
Ok(Self {
endpoint,
connection: None,
network_state: Spake2StateMachine::new(CryptoData::new()),
network_state: NetworkStateMachine::new(CryptoData::new()),
crypto_provider,
local_cert_der: cert_der,
})
Expand Down
6 changes: 3 additions & 3 deletions openscreen-quinn/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use anyhow::{Context, Result};
use openscreen_crypto::CryptoProvider;
use openscreen_crypto_rustcrypto::RustCryptoCryptoProvider;
use openscreen_network::{
state_machine::Spake2StateMachine, CryptoData, NetworkInput, NetworkOutput,
state_machine::NetworkStateMachine, CryptoData, NetworkInput, NetworkOutput,
};
use rustls::pki_types::CertificateDer;
use sha2::{Digest, Sha256};
Expand Down Expand Up @@ -273,7 +273,7 @@ impl QuinnServer {
debug!("[CONN:{}] Auth token configured for validation", conn_id);
}

let mut network_state = Spake2StateMachine::new(crypto_data);
let mut network_state = NetworkStateMachine::new(crypto_data);
let crypto_provider = RustCryptoCryptoProvider::new();

// Send initial auth-capabilities on connection
Expand Down Expand Up @@ -321,7 +321,7 @@ impl QuinnServer {
async fn drive_authentication(
connection: quinn::Connection,
conn_id: String,
mut network_state: Spake2StateMachine,
mut network_state: NetworkStateMachine,
mut crypto_provider: RustCryptoCryptoProvider,
) -> Result<()> {
let mut iteration = 0u64;
Expand Down