Merge remote-tracking branch 'origin/master'
Some checks failed
CI / checks (push) Failing after 1m51s

This commit is contained in:
Alois 2026-07-02 23:12:37 +02:00
commit 1b60ccfe43
9 changed files with 385 additions and 107 deletions

View file

@ -22,6 +22,7 @@ pub struct ClientConfig {
pub url: String, pub url: String,
pub tls: ClientTlsConfig, pub tls: ClientTlsConfig,
pub client_id: u64, pub client_id: u64,
pub description: Option<String>,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_timeout: Duration, pub auth_timeout: Duration,
} }
@ -38,6 +39,7 @@ impl ClientConfig {
url: url.into(), url: url.into(),
tls: ClientTlsConfig::SystemRoots, tls: ClientTlsConfig::SystemRoots,
client_id: 0, client_id: 0,
description: None,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
auth_timeout: Duration::from_secs(30), auth_timeout: Duration::from_secs(30),
} }
@ -57,6 +59,11 @@ impl ClientConfig {
self self
} }
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self { pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
self.auth_timeout = timeout; self.auth_timeout = timeout;
@ -76,6 +83,7 @@ pub struct MTPConnection {
pub version: Version, pub version: Version,
pub sender: Sender, pub sender: Sender,
pub receiver: Receiver, pub receiver: Receiver,
pub description: Option<String>,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_state: AuthState, pub auth_state: AuthState,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -150,12 +158,15 @@ impl MTPClient {
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?; mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
let version_str = format!("{}", PROTOCOL_VERSION); 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::Version, DataValue::Str(version_str))
.add_typed_default( .add_typed_default(
DataType::Id, DataType::Id,
DataValue::UnsignedNumber(config.client_id.into()), 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?; sender.send(&ident).await?;
@ -163,6 +174,7 @@ impl MTPClient {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
sender, sender,
receiver, receiver,
description: config.description,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
auth_state: AuthState::Unauthenticated, auth_state: AuthState::Unauthenticated,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -379,12 +391,15 @@ impl MTPClient {
let version_str = format!("{}", PROTOCOL_VERSION); let version_str = format!("{}", PROTOCOL_VERSION);
// 1. Send the unsigned Identification hello (version + claimed id). // 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::Version, DataValue::Str(version_str.clone()))
.add_typed_default( .add_typed_default(
DataType::Id, DataType::Id,
DataValue::UnsignedNumber(config.client_id as u128), 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 { if let Err(e) = sender.send(&ident).await {
sender.close(); sender.close();
return Err(e); return Err(e);
@ -464,6 +479,7 @@ impl MTPClient {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
sender, sender,
receiver, receiver,
description: config.description,
auth_state: AuthState::Authenticated, auth_state: AuthState::Authenticated,
client_id: config.client_id, client_id: config.client_id,
}) })
@ -519,9 +535,12 @@ impl MTPClient {
let pk_bytes = pk_bundle.as_bytes(); let pk_bytes = pk_bundle.as_bytes();
// 1. Send the unsigned Register hello (version + public-key bundle). // 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::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 {
register = register.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
}
if let Err(e) = sender.send(&register).await { if let Err(e) = sender.send(&register).await {
sender.close(); sender.close();
return Err(e); return Err(e);
@ -607,6 +626,7 @@ impl MTPClient {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
sender, sender,
receiver, receiver,
description: config.description,
auth_state: AuthState::Authenticated, auth_state: AuthState::Authenticated,
client_id: assigned_id, client_id: assigned_id,
}) })

View file

@ -71,7 +71,8 @@ The host's `accept()` method:
### Login/Register Handshake ### 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: **challenge-response**. The client speaks first with an *unsigned* hello:
- **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID - **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID

View file

@ -29,6 +29,7 @@ let config = ClientConfig::new("https://host.example.com:4433")
| `url` | `String` | `https://host:port` address of the MTP host | | `url` | `String` | `https://host:port` address of the MTP host |
| `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` | | `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` |
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) | | `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
| `description` | `Option<String>` | Optional label sent during handshake (e.g. `"phone"`) |
| `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) | | `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) |
### TLS Certificate Handling ### TLS Certificate Handling
@ -60,6 +61,7 @@ pub struct MTPConnection {
pub version: Version, pub version: Version,
pub sender: Sender, pub sender: Sender,
pub receiver: Receiver, pub receiver: Receiver,
pub description: Option<String>,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_state: AuthState, pub auth_state: AuthState,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -69,6 +71,7 @@ pub struct MTPConnection {
- `version` -- the negotiated protocol version - `version` -- the negotiated protocol version
- `sender` / `receiver` -- for message I/O - `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) - `client_id` -- the confirmed/assigned client identifier (crypto only)
### Unauthenticated Connect ### Unauthenticated Connect

View file

@ -48,11 +48,30 @@ let config = HostConfig::new(
| `port` | `u16` | Listen port | | `port` | `u16` | Listen port |
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain | | `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key | | `tls_key` | `Vec<u8>` | 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 | | `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
| `get_existing_user` | `Fn(u64) -> Pin<Box<dyn Future<Output = Option<PublicKeyBundle>> + Send>> + Send + Sync` (crypto) | Async lookup callback for login | | `get_existing_user` | `Fn(u64) -> Pin<Box<dyn Future<Output = Option<PublicKeyBundle>> + Send>> + Send + Sync` (crypto) | Async lookup callback for login |
| `complete_register` | `Fn(PublicKeyBundle) -> Pin<Box<dyn Future<Output = u64> + Send>> + Send + Sync` (crypto) | Async registration callback, returns new client ID | | `complete_register` | `Fn(PublicKeyBundle) -> Pin<Box<dyn Future<Output = u64> + 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 ### TLS
The host requires a TLS certificate. For development, generate a self-signed The host requires a TLS certificate. For development, generate a self-signed
@ -82,6 +101,7 @@ pub struct MTPConnection {
pub codec: VersionedCodec, pub codec: VersionedCodec,
pub sender: Sender, pub sender: Sender,
pub receiver: Receiver, pub receiver: Receiver,
pub description: Option<String>,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_state: AuthState, pub auth_state: AuthState,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -95,6 +115,7 @@ pub struct MTPConnection {
- `codec` -- a `VersionedCodec` scoped to the negotiated version (use for - `codec` -- a `VersionedCodec` scoped to the negotiated version (use for
version-aware encode/decode) version-aware encode/decode)
- `sender` / `receiver` -- for message I/O - `sender` / `receiver` -- for message I/O
- `description` -- optional client-provided label (e.g. `"phone"`, `"desktop"`)
- `client_id` -- the authenticated client's ID - `client_id` -- the authenticated client's ID
- `client_public_key` -- the client's public key bundle (for signature - `client_public_key` -- the client's public key bundle (for signature
verification of subsequent messages) verification of subsequent messages)
@ -130,7 +151,7 @@ let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
## Authentication Flow ## 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 **challenge-response** handshake before returning the connection. The host issues
a fresh, random `server_challenge` that the client must sign, which is what makes 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 the client's proof unreplayable: a captured proof is bound to a one-time challenge

View file

@ -41,7 +41,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Connecting to 127.0.0.1:8080 ..."); 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 server_bundle = host_public_key.clone();
let (conn, keyring) = let (conn, keyring) =

View file

@ -98,8 +98,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Server listening on {}", host.local_addr()); println!("Server listening on {}", host.local_addr());
while let Some(conn) = host.accept().await? { while let Some(conn) = host.accept().await? {
let desc = conn
.description
.as_deref()
.unwrap_or("(no description)");
println!( println!(
"\n--- New authenticated connection (version {}) ---", "\n--- New connection (version {}, description: {desc}) ---",
conn.version conn.version
); );
println!("Client ID: {}", conn.client_id); println!("Client ID: {}", conn.client_id);

View file

@ -29,6 +29,14 @@ type CompleteRegister = Box<
+ Sync, + Sync,
>; >;
#[cfg(feature = "crypto")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthenticationPolicy {
ForceAuthentication,
AllowAuthentication,
Unauthenticated,
}
/* Host configuration. */ /* Host configuration. */
pub struct HostConfig { pub struct HostConfig {
pub ip: IpAddr, pub ip: IpAddr,
@ -37,7 +45,7 @@ pub struct HostConfig {
pub tls_key: Vec<u8>, pub tls_key: Vec<u8>,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub require_authentication: bool, pub authentication_policy: AuthenticationPolicy,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_timeout: Duration, pub auth_timeout: Duration,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -56,7 +64,7 @@ impl HostConfig {
tls_fullchain, tls_fullchain,
tls_key, tls_key,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
require_authentication: false, authentication_policy: AuthenticationPolicy::Unauthenticated,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
auth_timeout: Duration::from_secs(30), auth_timeout: Duration::from_secs(30),
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -93,13 +101,19 @@ impl HostConfig {
+ Sync + Sync
+ 'static, + 'static,
) -> Self { ) -> Self {
self.require_authentication = true; self.authentication_policy = AuthenticationPolicy::ForceAuthentication;
self.host_keyring = host_keyring; self.host_keyring = host_keyring;
self.get_existing_user = Box::new(get_existing_user); self.get_existing_user = Box::new(get_existing_user);
self.complete_register = Box::new(complete_register); self.complete_register = Box::new(complete_register);
self self
} }
#[cfg(feature = "crypto")]
pub fn with_authentication_policy(mut self, policy: AuthenticationPolicy) -> Self {
self.authentication_policy = policy;
self
}
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self { pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
self.auth_timeout = timeout; self.auth_timeout = timeout;
@ -152,6 +166,7 @@ pub struct MTPConnection {
pub codec: VersionedCodec, pub codec: VersionedCodec,
pub sender: Sender, pub sender: Sender,
pub receiver: Receiver, pub receiver: Receiver,
pub description: Option<String>,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
pub auth_state: AuthState, pub auth_state: AuthState,
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
@ -203,49 +218,94 @@ impl MTPHost {
}; };
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
if self.config.require_authentication { match self.config.authentication_policy {
let timeout = self.config.auth_timeout; AuthenticationPolicy::ForceAuthentication => {
return match tokio::time::timeout(timeout, self.accept_authenticated(sender, receiver)) let timeout = self.config.auth_timeout;
return match tokio::time::timeout(
timeout,
self.accept_authenticated(sender, receiver),
)
.await .await
{ {
Ok(result) => result, Ok(result) => result,
Err(_) => Err(AcceptError::AuthenticationTimedOut), 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). // Non-crypto fallback: no authentication feature, just read and respond.
let first_msg = match receiver.receive().await { #[cfg(not(feature = "crypto"))]
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, let first_msg = match receiver.receive().await {
None => return Err(AcceptError::UnsupportedVersion(client_version)), Ok(m) => m,
}; Err(e) => return Err(AcceptError::Receive(e)),
};
let codec = VersionedCodec::new(self.registry.clone()); let client_version = match extract_version(&first_msg) {
Some(v) => v,
Ok(Some(MTPConnection { None => return Err(AcceptError::MissingVersion),
version: negotiated, };
codec, let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version))
sender, {
receiver, Some(v) => v,
#[cfg(feature = "crypto")] None => return Err(AcceptError::UnsupportedVersion(client_version)),
auth_state: AuthState::Unauthenticated, };
#[cfg(feature = "crypto")] let codec = VersionedCodec::new(self.registry.clone());
client_id: 0, let description = match first_msg.get_data(DataType::Description) {
#[cfg(feature = "crypto")] DataValue::Str(s) => Some(s.clone()),
client_public_key: None, _ => None,
})) };
return Ok(Some(MTPConnection {
version: negotiated,
codec,
sender,
receiver,
description,
}));
}
} }
pub fn local_addr(&self) -> std::net::SocketAddr { 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<u8>,
},
}
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
impl MTPHost { impl MTPHost {
/* /*
@ -278,51 +350,9 @@ impl MTPHost {
sender: Sender, sender: Sender,
receiver: Receiver, receiver: Receiver,
) -> Result<Option<MTPConnection>, AcceptError> { ) -> Result<Option<MTPConnection>, AcceptError> {
use mtp_crypto::{ use mtp_crypto::PublicKeyBundle;
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<u8>,
},
}
let tm = mtp_codec::TypeMap::latest(); 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<u8>, Vec<u8>), 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 ===== // ===== Step 1: receive the client's unsigned hello =====
let hello = match receiver.receive().await { 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) = let (flow, response_type) =
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) { if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
let cid = match hello.get_data(DataType::Id) { 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<String>,
) -> Result<Option<MTPConnection>, 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<u8>, Vec<u8>), 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 { let challenge_id = match &flow {
Flow::Login { id, .. } => *id, Flow::Login { id, .. } => *id,
Flow::Register { .. } => 0, Flow::Register { .. } => 0,
@ -464,18 +550,13 @@ impl MTPHost {
let (proof_payload, bundle) = match &flow { let (proof_payload, bundle) = match &flow {
Flow::Login { id, bundle } => ( 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, bundle,
), ),
Flow::Register { Flow::Register {
bundle, pk_bytes, .. bundle, pk_bytes, ..
} => ( } => (
auth::register_proof_payload( auth::register_proof_payload(version_str, pk_bytes, server_challenge, client_nonce),
&version_str,
pk_bytes,
server_challenge,
client_nonce,
),
bundle, bundle,
), ),
}; };
@ -551,11 +632,134 @@ impl MTPHost {
codec, codec,
sender, sender,
receiver, receiver,
description,
auth_state: AuthState::Authenticated, auth_state: AuthState::Authenticated,
client_id: assigned_id, client_id: assigned_id,
client_public_key: Some(client_bundle), 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<Option<MTPConnection>, 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(),
))
}
} }
/* /*

View file

@ -302,12 +302,15 @@ impl WasmClient {
.await?; .await?;
let version_str = format!("{}", PROTOCOL_VERSION); 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::Version, DataValue::Str(version_str))
.add_typed_default( .add_typed_default(
DataType::Id, DataType::Id,
DataValue::UnsignedNumber(config.client_id as u128), 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 let ident_bytes = ident
.to_bytes() .to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?; .map_err(|e| js_error(&format!("encode failed: {}", e)))?;
@ -351,12 +354,16 @@ impl WasmClient {
.await?; .await?;
// 1. Send the unsigned Identification hello. // 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::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() .to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?; .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. // 2. Receive and verify the host's challenge.
let server_challenge = self let server_challenge = self
@ -463,12 +470,16 @@ impl WasmClient {
.await?; .await?;
// 1. Send the unsigned Register hello (version + public-key bundle). // 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::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() .to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?; .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). // 2. Receive and verify the host's challenge (register binds id = 0).
let server_challenge = self let server_challenge = self

View file

@ -6,6 +6,7 @@ pub struct ConnectionConfig {
pub(crate) server_certificate_hashes: Option<Vec<String>>, pub(crate) server_certificate_hashes: Option<Vec<String>>,
pub(crate) client_id: u64, pub(crate) client_id: u64,
pub(crate) max_message_size: u32, pub(crate) max_message_size: u32,
pub(crate) description: Option<String>,
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -17,6 +18,7 @@ impl ConnectionConfig {
server_certificate_hashes: None, server_certificate_hashes: None,
client_id: 0, client_id: 0,
max_message_size: 1_000_000_000, max_message_size: 1_000_000_000,
description: None,
} }
} }
@ -49,4 +51,14 @@ impl ConnectionConfig {
pub fn max_message_size(&self) -> u32 { pub fn max_message_size(&self) -> u32 {
self.max_message_size 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<String> {
self.description.clone()
}
} }