# MTP Native Host The native host is a Rust library (`mtp-host`) that runs a QUIC server, accepts MTP client connections, negotiates protocol versions, and optionally performs a mutual-authentication handshake (login/register) using Ed25519 and ML-DSA-65 signatures. ## Cargo Dependency ```toml [dependencies] mtp = { path = "/path/to/mtp", features = ["host"] } # Add crypto for authenticated connections: mtp = { path = "/path/to/mtp", features = ["host", "crypto"] } ``` ## HostConfig ```rust use mtp::host::HostConfig; use std::net::{IpAddr, Ipv4Addr}; let config = HostConfig::new( IpAddr::V4(Ipv4Addr::UNSPECIFIED), 4433, std::fs::read("cert.pem")?, std::fs::read("key.pem")?, ) .with_authentication( /* Keyring */, |client_id: u64| { let db = CLIENT_DB.clone(); Box::pin(async move { db.lock().unwrap().get(&client_id).cloned() }) }, |bundle: PublicKeyBundle| { let mut db = CLIENT_DB.lock().unwrap(); let id = next_id(); db.insert(id, bundle); Box::pin(async move { id }) }, ); ``` | Field | Type | Description | |------------------------|-----------------------------------------|------------------------------------------------| | `ip` | `IpAddr` | Bind address | | `port` | `u16` | Listen port | | `tls_fullchain` | `Vec` | PEM-encoded TLS certificate chain | | `tls_key` | `Vec` | PEM-encoded TLS private key | | `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 certificate using `rcgen`. For production, use a CA-signed certificate. ## Accepting Connections ```rust use mtp::host::MTPHost; let mut host = MTPHost::new(config).await?; println!("Listening on {}", host.local_addr()); while let Some(conn) = host.accept().await? { // conn is an MTPConnection ready for I/O } ``` ### MTPConnection Returned by `accept()` after version negotiation (and authentication if enabled): ```rust pub struct MTPConnection { pub version: Version, pub codec: VersionedCodec, pub sender: Sender, pub receiver: Receiver, pub description: Option, #[cfg(feature = "crypto")] pub auth_state: AuthState, #[cfg(feature = "crypto")] pub client_id: u64, #[cfg(feature = "crypto")] pub client_public_key: Option, } ``` - `version` -- the negotiated protocol version - `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) ## Version Negotiation When a client connects, `accept()` performs the following sequence: 1. Accept the QUIC connection 2. Read the client's first `CommunicationValue` (always encoded with reserved type IDs) 3. Extract the protocol version from `DataType::Version` (reserved data type ID 0) as a `DataValue::Str("major.minor")` 4. Call `registry.negotiate(&[client_version])` to find the highest mutually supported version 5. Return an `AcceptError` (closing the connection) if no compatible version exists 6. Return `Ok(Some(MTPConnection))` with the negotiated version The `Registry` is built automatically from all type maps defined in your `type-maps.yaml` via `Registry::builtin()`. ### Registry ```rust use mtp::codec::registry::Registry; let registry = host.registry(); assert!(registry.supports(&Version(2, 0))); let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]); // -> Some(Version(2, 0)) if both versions are registered ``` ## Authentication Flow 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 the host generates per connection and will never reissue. The challenge lives only on the accepting task's stack; there is no replay database or shared state. All signed payloads begin with a one-byte domain-separation tag (see `mtp::crypto::auth`) so a signature for one step can never be reused as another. ### Login ``` Client Host | | | QUIC connect | |---------------------------------------->| | | | Identification { Version, Id } | (unsigned hello) |---------------------------------------->| | | lookup get_existing_user(id) | | generate random server_challenge | Challenge { | | ServerNonce(server_challenge), | | Signature, [PqSignature] | host signs the challenge | } | |<----------------------------------------| | ChallengeResponse { | | ClientNonce, Signature, [PqSignature]| client signs the challenge | } | |---------------------------------------->| | | verify proof over server_challenge | IdentificationResponse { | | Connected=true, Id, | | ClientNonce(echoed), | | Signature, [PqSignature] | | } | |<----------------------------------------| ``` Payloads (`||` is concatenation, integers big-endian; `DS_*` are domain tags): - Host challenge: `DS_CHALLENGE || id (8) || server_challenge (16)` - Client proof: `DS_LOGIN_PROOF || version_string || id (8) || server_challenge (16) || client_nonce (16)` - Host final: `DS_HOST_FINAL || assigned_id (8) || client_nonce (16) || server_challenge (16)` ### Register ``` Client Host | | | QUIC connect | |---------------------------------------->| | | | Register { | | Version, | (unsigned hello) | PublicKeys (serialized PublicKeyBundle) | } | |---------------------------------------->| | | generate random server_challenge | Challenge { | | ServerNonce(server_challenge), | | Signature, [PqSignature] | (challenge binds id = 0) | } | |<----------------------------------------| | ChallengeResponse { | | ClientNonce, Signature, [PqSignature]| | } | |---------------------------------------->| | | verify proof over server_challenge | | call complete_register(bundle) -> new_id | RegisterResponse { | | Connected=true, Id(new_id), | | ClientNonce(echoed), | | Signature, [PqSignature] | | } | |<----------------------------------------| ``` The register client proof is: `DS_REGISTER_PROOF || version_string || server_challenge (16) || client_nonce (16) || public_key_bytes` After a successful handshake, `accept()` returns an `MTPConnection` with `auth_state = Authenticated`, `client_id` set, and `client_public_key` available for verifying subsequent signed messages from the client. ### Rejection If verification fails or the client is not found (login), the host sends a rejection response with `Connected=false` and closes the send stream, returning `AcceptError::AuthenticationFailed` from `accept()`. ## Handling Messages Use `conn.sender` and `conn.receiver` for bidirectional message exchange: ```rust while let Some(conn) = host.accept().await? { tokio::spawn(async move { loop { match conn.receiver.receive().await { Ok(msg) => { let response = process_message(&msg, &conn); conn.sender.send(&response).await.ok(); } Err(_) => break, } } }); } ``` ### Versioned Codec The `conn.codec` is a `VersionedCodec` pre-configured with the negotiated version. Use it to encode/decode with version-specific type maps: ```rust let tm = conn.codec.registry().get(&conn.version).unwrap(); // Look up type IDs for the negotiated version let desc_id = DataTypeId(tm.data_id_enum(DataType::Description).unwrap()); let value = msg.get_data(desc_id); ``` ## Host Callbacks ### get_existing_user Called during login to retrieve a client's public key bundle for signature verification. Must return `Some(PublicKeyBundle)` if the client ID is known, or `None` to reject. ```rust let get_existing_user = |id: u64| { let db = db.clone(); Box::pin(async move { db.lock().unwrap().get(&id).cloned() }) }; ``` ### complete_register Called during registration to persist a new client's public key bundle and assign a client ID. The returned `u64` becomes the client's permanent identifier. ```rust let complete_register = |bundle: PublicKeyBundle| { let db = db.clone(); let id = next_id.fetch_add(1, Ordering::SeqCst); Box::pin(async move { db.lock().unwrap().insert(id, bundle); id }) }; ``` Both callbacks are called from within `accept()` and must be `Send + Sync`. They are `async` (returning `Pin>`) and are `.await`ed by the host, so they can perform I/O or other async work as needed. ## Host Key Generation Generate a host keyring once and persist it: ```rust use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner}; use mtp::crypto::kem::HybridKem; let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate(); let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate(); let (kem_sk, kem_pk) = HybridKem::generate_keypair(); let host_keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk); // Save to disk let bytes = host_keyring.to_bytes(); std::fs::write("host_keys.bin", bytes)?; ``` Export the public key bundle so clients can verify the host identity: ```rust let bundle = host_keyring.public_key_bundle(); std::fs::write("host_enc_kem_pk.bin", bundle.kem_public_key.as_bytes())?; std::fs::write("host_sig_pk.bin", bundle.sig_cl_public_key.as_bytes())?; std::fs::write("host_sig_pq_pk.bin", bundle.sig_pq_public_key.as_bytes())?; ``` ## Policy The transport `Policy` is set to defaults internally. To customise (timeouts, send mode, etc.), use `mtp_transport::host()` directly instead of `MTPHost`: ```rust use mtp_transport::{host, Policy}; let transport = host(ip, port, cert, key, custom_policy).await?; // Then build version negotiation on top: // - accept transport.next() // - read first frame // - registry.negotiate() // - return MTPConnection ``` ## Graceful Shutdown Drop the `MTPHost` to stop accepting new connections. Active connections continue until their `Sender`/`Receiver` are dropped or the peer disconnects.