From 56903049b66d67a5a34f8b6fb78820f19771f2f4 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:39:40 +0200 Subject: [PATCH 1/2] [Add] Description [Add] AuthenticationPolicy on host --- client/src/lib.rs | 26 ++- host/src/lib.rs | 388 ++++++++++++++++++++++++++++++++++----------- wasm/src/client.rs | 25 ++- wasm/src/config.rs | 12 ++ 4 files changed, 349 insertions(+), 102 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index a60541c..e044997 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -22,6 +22,7 @@ pub struct ClientConfig { pub url: String, pub tls: ClientTlsConfig, pub client_id: u64, + pub description: Option, #[cfg(feature = "crypto")] pub auth_timeout: Duration, } @@ -38,6 +39,7 @@ impl ClientConfig { url: url.into(), tls: ClientTlsConfig::SystemRoots, client_id: 0, + description: None, #[cfg(feature = "crypto")] auth_timeout: Duration::from_secs(30), } @@ -57,6 +59,11 @@ impl ClientConfig { self } + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + #[cfg(feature = "crypto")] pub fn with_auth_timeout(mut self, timeout: Duration) -> Self { self.auth_timeout = timeout; @@ -76,6 +83,7 @@ pub struct MTPConnection { pub version: Version, pub sender: Sender, pub receiver: Receiver, + pub description: Option, #[cfg(feature = "crypto")] pub auth_state: AuthState, #[cfg(feature = "crypto")] @@ -150,12 +158,15 @@ impl MTPClient { mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?; let version_str = format!("{}", PROTOCOL_VERSION); - let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) + let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) .add_typed_default(DataType::Version, DataValue::Str(version_str)) .add_typed_default( DataType::Id, DataValue::UnsignedNumber(config.client_id.into()), ); + if let Some(desc) = &config.description { + ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone())); + } sender.send(&ident).await?; @@ -163,6 +174,7 @@ impl MTPClient { version: PROTOCOL_VERSION, sender, receiver, + description: config.description, #[cfg(feature = "crypto")] auth_state: AuthState::Unauthenticated, #[cfg(feature = "crypto")] @@ -379,12 +391,15 @@ impl MTPClient { let version_str = format!("{}", PROTOCOL_VERSION); // 1. Send the unsigned Identification hello (version + claimed id). - let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) + let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) .add_typed_default(DataType::Version, DataValue::Str(version_str.clone())) .add_typed_default( DataType::Id, DataValue::UnsignedNumber(config.client_id as u128), ); + if let Some(desc) = &config.description { + ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone())); + } if let Err(e) = sender.send(&ident).await { sender.close(); return Err(e); @@ -464,6 +479,7 @@ impl MTPClient { version: PROTOCOL_VERSION, sender, receiver, + description: config.description, auth_state: AuthState::Authenticated, client_id: config.client_id, }) @@ -519,9 +535,12 @@ impl MTPClient { let pk_bytes = pk_bundle.as_bytes(); // 1. Send the unsigned Register hello (version + public-key bundle). - let register = CommunicationValue::new(mtp_codec::CommunicationType::Register) + let mut register = CommunicationValue::new(mtp_codec::CommunicationType::Register) .add_typed_default(DataType::Version, DataValue::Str(version_str.clone())) .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone())); + if let Some(desc) = &config.description { + register = register.add_typed_default(DataType::Description, DataValue::Str(desc.clone())); + } if let Err(e) = sender.send(®ister).await { sender.close(); return Err(e); @@ -607,6 +626,7 @@ impl MTPClient { version: PROTOCOL_VERSION, sender, receiver, + description: config.description, auth_state: AuthState::Authenticated, client_id: assigned_id, }) diff --git a/host/src/lib.rs b/host/src/lib.rs index 389af78..294a537 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -29,6 +29,14 @@ type CompleteRegister = Box< + Sync, >; +#[cfg(feature = "crypto")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AuthenticationPolicy { + ForceAuthentication, + AllowAuthentication, + Unauthenticated, +} + /* Host configuration. */ pub struct HostConfig { pub ip: IpAddr, @@ -37,7 +45,7 @@ pub struct HostConfig { pub tls_key: Vec, #[cfg(feature = "crypto")] - pub require_authentication: bool, + pub authentication_policy: AuthenticationPolicy, #[cfg(feature = "crypto")] pub auth_timeout: Duration, #[cfg(feature = "crypto")] @@ -56,7 +64,7 @@ impl HostConfig { tls_fullchain, tls_key, #[cfg(feature = "crypto")] - require_authentication: false, + authentication_policy: AuthenticationPolicy::Unauthenticated, #[cfg(feature = "crypto")] auth_timeout: Duration::from_secs(30), #[cfg(feature = "crypto")] @@ -93,13 +101,19 @@ impl HostConfig { + Sync + 'static, ) -> Self { - self.require_authentication = true; + self.authentication_policy = AuthenticationPolicy::ForceAuthentication; self.host_keyring = host_keyring; self.get_existing_user = Box::new(get_existing_user); self.complete_register = Box::new(complete_register); self } + #[cfg(feature = "crypto")] + pub fn with_authentication_policy(mut self, policy: AuthenticationPolicy) -> Self { + self.authentication_policy = policy; + self + } + #[cfg(feature = "crypto")] pub fn with_auth_timeout(mut self, timeout: Duration) -> Self { self.auth_timeout = timeout; @@ -152,6 +166,7 @@ pub struct MTPConnection { pub codec: VersionedCodec, pub sender: Sender, pub receiver: Receiver, + pub description: Option, #[cfg(feature = "crypto")] pub auth_state: AuthState, #[cfg(feature = "crypto")] @@ -203,49 +218,94 @@ impl MTPHost { }; #[cfg(feature = "crypto")] - if self.config.require_authentication { - let timeout = self.config.auth_timeout; - return match tokio::time::timeout(timeout, self.accept_authenticated(sender, receiver)) + match self.config.authentication_policy { + AuthenticationPolicy::ForceAuthentication => { + let timeout = self.config.auth_timeout; + return match tokio::time::timeout( + timeout, + self.accept_authenticated(sender, receiver), + ) .await - { - Ok(result) => result, - Err(_) => Err(AcceptError::AuthenticationTimedOut), - }; + { + Ok(result) => result, + Err(_) => Err(AcceptError::AuthenticationTimedOut), + }; + } + AuthenticationPolicy::AllowAuthentication => { + return self.accept_allow_auth(sender, receiver).await; + } + AuthenticationPolicy::Unauthenticated => { + let first_msg = match receiver.receive().await { + Ok(m) => m, + Err(e) => return Err(AcceptError::Receive(e)), + }; + if first_msg.get_type() + == mtp_codec::CommunicationType::Register.to_id(&mtp_codec::TypeMap::latest()) + { + sender.close(); + return Err(AcceptError::AuthenticationFailed( + "authentication not allowed on this host".into(), + )); + } + let client_version = match extract_version(&first_msg) { + Some(v) => v, + None => return Err(AcceptError::MissingVersion), + }; + let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version)) + { + Some(v) => v, + None => return Err(AcceptError::UnsupportedVersion(client_version)), + }; + let codec = VersionedCodec::new(self.registry.clone()); + let description = match first_msg.get_data(DataType::Description) { + DataValue::Str(s) => Some(s.clone()), + _ => None, + }; + Ok(Some(MTPConnection { + version: negotiated, + codec, + sender, + receiver, + description, + #[cfg(feature = "crypto")] + auth_state: AuthState::Unauthenticated, + #[cfg(feature = "crypto")] + client_id: rand::random(), + #[cfg(feature = "crypto")] + client_public_key: None, + })) + } } - // Read the first message (always encoded with reserved types). - let first_msg = match receiver.receive().await { - Ok(m) => m, - Err(e) => return Err(AcceptError::Receive(e)), - }; - - let client_version = match extract_version(&first_msg) { - Some(v) => v, - None => return Err(AcceptError::MissingVersion), - }; - - let negotiated = match self - .registry - .negotiate(std::slice::from_ref(&client_version)) + // Non-crypto fallback: no authentication feature, just read and respond. + #[cfg(not(feature = "crypto"))] { - Some(v) => v, - None => return Err(AcceptError::UnsupportedVersion(client_version)), - }; - - let codec = VersionedCodec::new(self.registry.clone()); - - Ok(Some(MTPConnection { - version: negotiated, - codec, - sender, - receiver, - #[cfg(feature = "crypto")] - auth_state: AuthState::Unauthenticated, - #[cfg(feature = "crypto")] - client_id: 0, - #[cfg(feature = "crypto")] - client_public_key: None, - })) + let first_msg = match receiver.receive().await { + Ok(m) => m, + Err(e) => return Err(AcceptError::Receive(e)), + }; + let client_version = match extract_version(&first_msg) { + Some(v) => v, + None => return Err(AcceptError::MissingVersion), + }; + let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version)) + { + Some(v) => v, + None => return Err(AcceptError::UnsupportedVersion(client_version)), + }; + let codec = VersionedCodec::new(self.registry.clone()); + let description = match first_msg.get_data(DataType::Description) { + DataValue::Str(s) => Some(s.clone()), + _ => None, + }; + return Ok(Some(MTPConnection { + version: negotiated, + codec, + sender, + receiver, + description, + })); + } } pub fn local_addr(&self) -> std::net::SocketAddr { @@ -257,6 +317,18 @@ impl MTPHost { } } +#[cfg(feature = "crypto")] +enum Flow { + Login { + id: u64, + bundle: mtp_crypto::PublicKeyBundle, + }, + Register { + bundle: mtp_crypto::PublicKeyBundle, + pk_bytes: Vec, + }, +} + #[cfg(feature = "crypto")] impl MTPHost { /* @@ -278,51 +350,9 @@ impl MTPHost { sender: Sender, receiver: Receiver, ) -> Result, AcceptError> { - use mtp_crypto::{ - Ed25519Signer, MlDsaSigner, PublicKeyBundle, SignatureScheme, auth, verify_ed25519, - verify_ml_dsa, - }; - - // Flow-specific state resolved from the client's opening hello. - enum Flow { - Login { - id: u64, - bundle: PublicKeyBundle, - }, - Register { - bundle: PublicKeyBundle, - pk_bytes: Vec, - }, - } + use mtp_crypto::PublicKeyBundle; let tm = mtp_codec::TypeMap::latest(); - let pq_enabled = !self - .config - .host_keyring - .sig_pq_secret_key - .as_bytes() - .is_empty(); - - // Sign `payload` with the host keys (Ed25519 always, ML-DSA when configured). - let host_sign = |payload: &[u8]| -> Result<(Vec, Vec), AcceptError> { - let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key) - .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?; - let sig = signer - .sign(payload) - .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?; - let pq_sig = if pq_enabled { - let pq = MlDsaSigner::new( - &self.config.host_keyring.sig_pq_secret_key, - &self.config.host_keyring.sig_pq_public_key, - ) - .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?; - pq.sign(payload) - .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))? - } else { - Vec::new() - }; - Ok((sig, pq_sig)) - }; // ===== Step 1: receive the client's unsigned hello ===== let hello = match receiver.receive().await { @@ -347,6 +377,11 @@ impl MTPHost { } }; + let description = match hello.get_data(DataType::Description) { + DataValue::Str(s) => Some(s.clone()), + _ => None, + }; + let (flow, response_type) = if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) { let cid = match hello.get_data(DataType::Id) { @@ -400,6 +435,57 @@ impl MTPHost { )); }; + self.complete_auth_handshake(sender, receiver, flow, response_type, &version_str, client_version, description).await + } + + /* + * Steps 2-4 of the authenticated handshake, shared by both ForceAuthentication + * and AllowAuthentication. Takes the already-parsed hello (step 1) via `flow`, + * `version_str`, and `client_version`. + */ + #[allow(clippy::too_many_arguments)] + async fn complete_auth_handshake( + &self, + sender: Sender, + receiver: Receiver, + flow: Flow, + response_type: mtp_codec::CommunicationType, + version_str: &str, + client_version: Version, + description: Option, + ) -> Result, AcceptError> { + use mtp_crypto::{ + auth, verify_ed25519, verify_ml_dsa, Ed25519Signer, MlDsaSigner, SignatureScheme, + }; + + let tm = mtp_codec::TypeMap::latest(); + let pq_enabled = !self + .config + .host_keyring + .sig_pq_secret_key + .as_bytes() + .is_empty(); + + let host_sign = |payload: &[u8]| -> Result<(Vec, Vec), AcceptError> { + let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key) + .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?; + let sig = signer + .sign(payload) + .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?; + let pq_sig = if pq_enabled { + let pq = MlDsaSigner::new( + &self.config.host_keyring.sig_pq_secret_key, + &self.config.host_keyring.sig_pq_public_key, + ) + .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?; + pq.sign(payload) + .map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))? + } else { + Vec::new() + }; + Ok((sig, pq_sig)) + }; + let challenge_id = match &flow { Flow::Login { id, .. } => *id, Flow::Register { .. } => 0, @@ -464,18 +550,13 @@ impl MTPHost { let (proof_payload, bundle) = match &flow { Flow::Login { id, bundle } => ( - auth::login_proof_payload(&version_str, *id, server_challenge, client_nonce), + auth::login_proof_payload(version_str, *id, server_challenge, client_nonce), bundle, ), Flow::Register { bundle, pk_bytes, .. } => ( - auth::register_proof_payload( - &version_str, - pk_bytes, - server_challenge, - client_nonce, - ), + auth::register_proof_payload(version_str, pk_bytes, server_challenge, client_nonce), bundle, ), }; @@ -551,11 +632,134 @@ impl MTPHost { codec, sender, receiver, + description, auth_state: AuthState::Authenticated, client_id: assigned_id, client_public_key: Some(client_bundle), })) } + + /* + * Allow-authentication accept: clients may connect with or without + * authentication. Register messages always trigger the auth handshake. + * Identification with a known client-id triggers login; otherwise the + * client is treated as unauthenticated with a random id. + */ + async fn accept_allow_auth( + &mut self, + sender: Sender, + receiver: Receiver, + ) -> Result, AcceptError> { + use mtp_crypto::PublicKeyBundle; + + let tm = mtp_codec::TypeMap::latest(); + + let hello = match receiver.receive().await { + Ok(m) => m, + Err(e) => return Err(AcceptError::Receive(e)), + }; + + let version_str = match hello.get_data(DataType::Version) { + DataValue::Str(s) => s.clone(), + _ => { + sender.close(); + return Err(AcceptError::MissingVersion); + } + }; + let client_version = match Version::parse(&version_str) { + Some(v) => v, + None => { + sender.close(); + return Err(AcceptError::MissingVersion); + } + }; + + let description = match hello.get_data(DataType::Description) { + DataValue::Str(s) => Some(s.clone()), + _ => None, + }; + + // Register → authenticated registration. + if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) { + let bundle = match hello.get_data(DataType::PublicKeys) { + DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).map_err(|_| { + sender.close(); + AcceptError::AuthenticationFailed("invalid public key bundle".into()) + })?, + _ => { + sender.close(); + return Err(AcceptError::AuthenticationFailed("missing public keys".into())); + } + }; + let pk_bytes = bundle.as_bytes(); + return self + .complete_auth_handshake( + sender, + receiver, + Flow::Register { + bundle, + pk_bytes, + }, + mtp_codec::CommunicationType::RegisterResponse, + &version_str, + client_version, + description, + ) + .await; + } + + // Identification with a known client-id → login. + if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) { + let cid = match hello.get_data(DataType::Id) { + DataValue::UnsignedNumber(n) => *n as u64, + _ => 0, + }; + + if cid > 0 + && let Some(bundle) = (self.config.get_existing_user)(cid).await + { + return self + .complete_auth_handshake( + sender, + receiver, + Flow::Login { + id: cid, + bundle, + }, + mtp_codec::CommunicationType::IdentificationResponse, + &version_str, + client_version, + description, + ) + .await; + } + + // Unknown (or zero) client id → unauthenticated connection. + let negotiated = match self + .registry + .negotiate(std::slice::from_ref(&client_version)) + { + Some(v) => v, + None => return Err(AcceptError::UnsupportedVersion(client_version)), + }; + let codec = VersionedCodec::new(self.registry.clone()); + return Ok(Some(MTPConnection { + version: negotiated, + codec, + sender, + receiver, + description, + auth_state: AuthState::Unauthenticated, + client_id: rand::random(), + client_public_key: None, + })); + } + + sender.close(); + Err(AcceptError::AuthenticationFailed( + "unexpected message type".into(), + )) + } } /* diff --git a/wasm/src/client.rs b/wasm/src/client.rs index 8fdde48..7aef180 100644 --- a/wasm/src/client.rs +++ b/wasm/src/client.rs @@ -302,12 +302,15 @@ impl WasmClient { .await?; let version_str = format!("{}", PROTOCOL_VERSION); - let ident = CommunicationValue::new(CommunicationType::Identification) + let mut ident = CommunicationValue::new(CommunicationType::Identification) .add_typed_default(DataType::Version, DataValue::Str(version_str)) .add_typed_default( DataType::Id, DataValue::UnsignedNumber(config.client_id as u128), ); + if let Some(desc) = &config.description { + ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone())); + } let ident_bytes = ident .to_bytes() .map_err(|e| js_error(&format!("encode failed: {}", e)))?; @@ -351,12 +354,16 @@ impl WasmClient { .await?; // 1. Send the unsigned Identification hello. - let hello = CommunicationValue::new(CommunicationType::Identification) + let mut hello = CommunicationValue::new(CommunicationType::Identification) .add_typed_default(DataType::Version, DataValue::Str(version_str.clone())) - .add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128)) + .add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128)); + if let Some(desc) = &config.description { + hello = hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone())); + } + let hello_bytes = hello .to_bytes() .map_err(|e| js_error(&format!("encode failed: {}", e)))?; - transport.send_frame(&hello).await?; + transport.send_frame(&hello_bytes).await?; // 2. Receive and verify the host's challenge. let server_challenge = self @@ -463,12 +470,16 @@ impl WasmClient { .await?; // 1. Send the unsigned Register hello (version + public-key bundle). - let hello = CommunicationValue::new(CommunicationType::Register) + let mut hello = CommunicationValue::new(CommunicationType::Register) .add_typed_default(DataType::Version, DataValue::Str(version_str.clone())) - .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone())) + .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone())); + if let Some(desc) = &config.description { + hello = hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone())); + } + let hello_bytes = hello .to_bytes() .map_err(|e| js_error(&format!("encode failed: {}", e)))?; - transport.send_frame(&hello).await?; + transport.send_frame(&hello_bytes).await?; // 2. Receive and verify the host's challenge (register binds id = 0). let server_challenge = self diff --git a/wasm/src/config.rs b/wasm/src/config.rs index 381d76e..8ac13b5 100644 --- a/wasm/src/config.rs +++ b/wasm/src/config.rs @@ -6,6 +6,7 @@ pub struct ConnectionConfig { pub(crate) server_certificate_hashes: Option>, pub(crate) client_id: u64, pub(crate) max_message_size: u32, + pub(crate) description: Option, } #[wasm_bindgen] @@ -17,6 +18,7 @@ impl ConnectionConfig { server_certificate_hashes: None, client_id: 0, max_message_size: 1_000_000_000, + description: None, } } @@ -49,4 +51,14 @@ impl ConnectionConfig { pub fn max_message_size(&self) -> u32 { self.max_message_size } + + #[wasm_bindgen(setter)] + pub fn set_description(&mut self, description: String) { + self.description = Some(description); + } + + #[wasm_bindgen(getter)] + pub fn description(&self) -> Option { + self.description.clone() + } } From 5bfcccc056a2a491a72e4315aa95effb2243ad3c Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:21:12 +0200 Subject: [PATCH 2/2] Desctiption-Docs --- docs/CONNECTOR.md | 3 ++- docs/NATIVE-CLIENT.md | 3 +++ docs/NATIVE-HOST.md | 25 +++++++++++++++++++++++-- example/client/src/main.rs | 4 +++- example/server/src/main.rs | 6 +++++- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/CONNECTOR.md b/docs/CONNECTOR.md index 0ef94c1..551b8c8 100644 --- a/docs/CONNECTOR.md +++ b/docs/CONNECTOR.md @@ -71,7 +71,8 @@ The host's `accept()` method: ### Login/Register Handshake -When `require_authentication` is set, the parties run a mutually-authenticated +When `authentication_policy` is `ForceAuthentication` or `AllowAuthentication`, +the parties run a mutually-authenticated **challenge-response**. The client speaks first with an *unsigned* hello: - **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID diff --git a/docs/NATIVE-CLIENT.md b/docs/NATIVE-CLIENT.md index 5f7d1ac..027aa29 100644 --- a/docs/NATIVE-CLIENT.md +++ b/docs/NATIVE-CLIENT.md @@ -29,6 +29,7 @@ let config = ClientConfig::new("https://host.example.com:4433") | `url` | `String` | `https://host:port` address of the MTP host | | `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` | | `client_id` | `u64` | Client identifier (ignored during `auth_register`) | +| `description` | `Option` | Optional label sent during handshake (e.g. `"phone"`) | | `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) | ### TLS Certificate Handling @@ -60,6 +61,7 @@ pub struct MTPConnection { pub version: Version, pub sender: Sender, pub receiver: Receiver, + pub description: Option, #[cfg(feature = "crypto")] pub auth_state: AuthState, #[cfg(feature = "crypto")] @@ -69,6 +71,7 @@ pub struct MTPConnection { - `version` -- the negotiated protocol version - `sender` / `receiver` -- for message I/O +- `description` -- the label sent during handshake (set via `ClientConfig::with_description`) - `client_id` -- the confirmed/assigned client identifier (crypto only) ### Unauthenticated Connect diff --git a/docs/NATIVE-HOST.md b/docs/NATIVE-HOST.md index caf92f9..631c1ea 100644 --- a/docs/NATIVE-HOST.md +++ b/docs/NATIVE-HOST.md @@ -48,11 +48,30 @@ let config = HostConfig::new( | `port` | `u16` | Listen port | | `tls_fullchain` | `Vec` | PEM-encoded TLS certificate chain | | `tls_key` | `Vec` | PEM-encoded TLS private key | -| `require_authentication` | `bool` (crypto) | Enable login/register handshake | +| `authentication_policy` | `AuthenticationPolicy` (crypto) | `ForceAuthentication`, `AllowAuthentication`, or `Unauthenticated` | | `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys | | `get_existing_user` | `Fn(u64) -> Pin> + Send>> + Send + Sync` (crypto) | Async lookup callback for login | | `complete_register` | `Fn(PublicKeyBundle) -> Pin + Send>> + Send + Sync` (crypto) | Async registration callback, returns new client ID | +### AuthenticationPolicy + +`ForceAuthentication` requires every client to complete the login/register handshake. `AllowAuthentication` accepts both authenticated and unauthenticated connections — unauthenticated clients get a random ID and `AuthState::Unauthenticated`. `Unauthenticated` rejects any client that tries to authenticate and is the default. + +```rust +use mtp::host::AuthenticationPolicy; + +// Force authentication (default was `require_authentication: true`): +let config = HostConfig::new(ip, port, cert, key) + .with_authentication(host_keyring, get_user, register); + +// Allow both authenticated and unauthenticated: +let config = HostConfig::new(ip, port, cert, key) + .with_allow_authentication(host_keyring, get_user, register); + +// Unauthenticated only (default): +let config = HostConfig::new(ip, port, cert, key); +``` + ### TLS The host requires a TLS certificate. For development, generate a self-signed @@ -82,6 +101,7 @@ pub struct MTPConnection { pub codec: VersionedCodec, pub sender: Sender, pub receiver: Receiver, + pub description: Option, #[cfg(feature = "crypto")] pub auth_state: AuthState, #[cfg(feature = "crypto")] @@ -95,6 +115,7 @@ pub struct MTPConnection { - `codec` -- a `VersionedCodec` scoped to the negotiated version (use for version-aware encode/decode) - `sender` / `receiver` -- for message I/O +- `description` -- optional client-provided label (e.g. `"phone"`, `"desktop"`) - `client_id` -- the authenticated client's ID - `client_public_key` -- the client's public key bundle (for signature verification of subsequent messages) @@ -130,7 +151,7 @@ let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]); ## Authentication Flow -When `require_authentication` is `true`, `accept()` runs a mutually-authenticated +When `authentication_policy` is `ForceAuthentication`, `accept()` runs a mutually-authenticated **challenge-response** handshake before returning the connection. The host issues a fresh, random `server_challenge` that the client must sign, which is what makes the client's proof unreplayable: a captured proof is bound to a one-time challenge diff --git a/example/client/src/main.rs b/example/client/src/main.rs index e940e0c..b44e05d 100644 --- a/example/client/src/main.rs +++ b/example/client/src/main.rs @@ -41,7 +41,9 @@ async fn main() -> Result<(), Box> { println!("Connecting to 127.0.0.1:8080 ..."); - let config = ClientConfig::new("https://127.0.0.1:8080").with_pinned_pem(cert_pem); + let config = ClientConfig::new("https://127.0.0.1:8080") + .with_pinned_pem(cert_pem) + .with_description("MTP example client"); let server_bundle = host_public_key.clone(); let (conn, keyring) = diff --git a/example/server/src/main.rs b/example/server/src/main.rs index a9854a2..3de4a97 100644 --- a/example/server/src/main.rs +++ b/example/server/src/main.rs @@ -98,8 +98,12 @@ async fn main() -> Result<(), Box> { println!("Server listening on {}", host.local_addr()); while let Some(conn) = host.accept().await? { + let desc = conn + .description + .as_deref() + .unwrap_or("(no description)"); println!( - "\n--- New authenticated connection (version {}) ---", + "\n--- New connection (version {}, description: {desc}) ---", conn.version ); println!("Client ID: {}", conn.client_id);