From f4118f28ba9edca0f6f0ab29e8685e7aed673b96 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:08:48 +0200 Subject: [PATCH] Host & Client force randomness on each other. Updated Reserved entry order. Made DataType ID changes easier in future (this MAY NOT happen again once in use). --- CONNECTOR.md | 24 +- Cargo.lock | 3 + Cargo.toml | 5 + NATIVE-CLIENT.md | 40 ++- NATIVE-HOST.md | 69 +++-- README.md | 2 +- WASM-CLIENT.md | 18 +- client/src/lib.rs | 432 ++++++++++++++------------- codec/src/communication_value.rs | 27 +- codec/src/data_value.rs | 132 ++++---- common/src/lib.rs | 12 +- crypto/README.md | 8 +- crypto/src/auth.rs | 128 ++++++++ crypto/src/kdf.rs | 6 +- crypto/src/keypair.rs | 2 +- crypto/src/lib.rs | 1 + example-usage/client/src/messages.rs | 17 +- host/src/lib.rs | 342 +++++++++++---------- transport/tests/integration.rs | 53 ++-- type-map/build.rs | 107 ++++--- type-map/src/lib.rs | 8 +- wasm/src/client.rs | 263 ++++++++++------ wasm/src/lib.rs | 1 + wasm/src/message.rs | 47 +-- wasm/src/transport.rs | 10 +- 25 files changed, 1061 insertions(+), 696 deletions(-) create mode 100644 crypto/src/auth.rs mode change 100644 => 100755 type-map/build.rs diff --git a/CONNECTOR.md b/CONNECTOR.md index 03b98b8..86f6948 100644 --- a/CONNECTOR.md +++ b/CONNECTOR.md @@ -68,14 +68,24 @@ The host's `accept()` method: 6. Returns `None` if the version is unsupported 7. Returns an `MTPConnection` with the negotiated version otherwise -### Login/Register Handshake (crypto feature) +### Login/Register Handshake -When `require_authentication` is set, the host sends a **greeting** first (host ID, public keys, nonce). The client then responds with either: +When `require_authentication` is set, the parties run a mutually-authenticated +**challenge-response**. The client speaks first with an *unsigned* hello: -- **Login** (`CommunicationType::Identification`, ID 15): client ID, nonce, signature -- **Register** (`CommunicationType::Register`, ID 17): public keys, nonce, signature +- **Login** (`CommunicationType::Identification`, ID 15): version, client ID +- **Register** (`CommunicationType::Register`, ID 17): version, public keys -The host verifies the client's signature, sends a signed response, and the client verifies the host's signature. +The host then issues a fresh random `server_challenge` in a signed `Challenge` +(`CommunicationType::Challenge`, ID 21, carrying `ServerNonce`). The client signs +that challenge, binding its id (login) or public keys (register), and returns a +`ChallengeResponse` (ID 22). The host verifies the proof against the challenge it +issued and sends a signed final response, which the client verifies. + +Because the client's proof covers the host-issued `server_challenge` (a one-time +value held only on the accepting task's stack), a captured proof cannot be +replayed on another connection. All signed payloads are domain-separated; see +`mtp::crypto::auth`. --- @@ -116,8 +126,8 @@ Client (v2.0) Host (v0.0, v1.0, v2.0) | CommValue{ Ident. } | | Version -> "2.0" | | Id -> 8765 | - | Nonce -> ... | - | Signature -> ... | + | (unsigned hello; auth | + | challenge follows) | |----------------------->| | | registry.negotiate(&[Version(2,0)]) | | -> Some(Version(2,0)) diff --git a/Cargo.lock b/Cargo.lock index c85e323..7352382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -976,6 +976,9 @@ dependencies = [ "mtp-host", "mtp-transport", "mtp-type-map", + "rand 0.8.6", + "rcgen", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8cc74c0..4c67127 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,3 +79,8 @@ host = ["dep:mtp-host", "mtp-codec/registry", "mtp-transport/host"] # MTP client - outgoing QUIC connections to a host. client = ["dep:mtp-client"] + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } +rcgen = "0.14" +rand = "0.8" diff --git a/NATIVE-CLIENT.md b/NATIVE-CLIENT.md index 3e54f5a..15e00c7 100644 --- a/NATIVE-CLIENT.md +++ b/NATIVE-CLIENT.md @@ -109,15 +109,22 @@ let config = ClientConfig { let conn = MTPClient::auth_connect(config, &keys, &host_pk).await?; ``` -Protocol: -1. Client generates a random nonce -2. Builds a signature payload: `version || client_id || client_nonce` -3. Signs with Ed25519 (and optionally ML-DSA-65) -4. Sends `Identification` frame containing version, client ID, nonce, signature(s) -5. Host responds with `IdentificationResponse` containing echoed nonce, host - nonce, and host signature +Protocol (challenge-response, the host issues the freshness): +1. Client sends an unsigned `Identification` hello (version, client ID) +2. Host replies with a `Challenge` carrying a fresh random `server_challenge` + and the host's signature over it; the client verifies that signature +3. Client generates a random `client_nonce` and signs + `version || client_id || server_challenge || client_nonce` with Ed25519 + (and optionally ML-DSA-65) +4. Client sends a `ChallengeResponse` frame (nonce + signature(s)) +5. Host verifies the proof against `server_challenge` and responds with + `IdentificationResponse` (echoed nonce + host signature) 6. Client verifies the host signature and nonce echo +Because the client's signature covers the host-issued `server_challenge`, a +captured proof cannot be replayed on another connection (each connection gets a +different challenge). + ### Registration ```rust @@ -134,13 +141,16 @@ let id = conn.client_id; let keyring_bytes = keyring.to_bytes(); ``` -Protocol: -1. Client generates a random nonce -2. Builds a signature payload: `version || client_nonce || public_key_bytes` -3. Signs with Ed25519 (and optionally ML-DSA-65) -4. Sends `Register` frame containing version, nonce, public key bundle, signature(s) -5. Host assigns a new client ID, responds with `RegisterResponse` containing - the ID, echoed nonce, host nonce, and host signature +Protocol (challenge-response): +1. Client sends an unsigned `Register` hello (version, public key bundle) +2. Host replies with a `Challenge` carrying a fresh random `server_challenge` + (signed by the host); the client verifies that signature +3. Client generates a random `client_nonce` and signs + `version || server_challenge || client_nonce || public_key_bytes` with + Ed25519 (and optionally ML-DSA-65) +4. Client sends a `ChallengeResponse` frame (nonce + signature(s)) +5. Host verifies the proof against `server_challenge`, assigns a new client ID, + and responds with `RegisterResponse` (the ID, echoed nonce, host signature) 6. Client verifies the host signature and nonce echo ## Key Material @@ -271,7 +281,7 @@ sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, &recipi ``` On the receiving side, the recipient decrypts with its own `Keyring` (each blob -is self-describing — its leading byte selects the algorithm and the matching KEM +is self-describing: its leading byte selects the algorithm and the matching KEM key from the keyring): ```rust diff --git a/NATIVE-HOST.md b/NATIVE-HOST.md index 94d095b..ab8cf5c 100644 --- a/NATIVE-HOST.md +++ b/NATIVE-HOST.md @@ -129,12 +129,19 @@ let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]); // -> Some(Version(2, 0)) if both versions are registered ``` -## Authentication Flow (crypto feature) +## Authentication Flow -When `require_authentication` is `true`, `accept()` runs an authenticated -handshake before returning the connection. The flow is: +When `require_authentication` is `true`, `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. -### Login (existing client) +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 @@ -142,26 +149,35 @@ Client Host | QUIC connect | |---------------------------------------->| | | - | Identification { | - | Version, Id, ClientNonce, | - | Signature, [PqSignature] | + | 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 | } | |---------------------------------------->| - | | lookup get_existing_user(client_id) - | | verify Ed25519 (and optional ML-DSA) sig + | | verify proof over server_challenge | IdentificationResponse { | - | Connected=true, ClientNonce(echoed), | - | Id, Timestamp(new_nonce), | + | Connected=true, Id, | + | ClientNonce(echoed), | | Signature, [PqSignature] | | } | |<----------------------------------------| ``` -The client signature payload is: `version_string || client_id (8 bytes, big-endian) || client_nonce (16 bytes, big-endian)` +Payloads (`||` is concatenation, integers big-endian; `DS_*` are domain tags): -The host signs: `0x01 || assigned_id (8 bytes, big-endian) || client_nonce (16 bytes) || host_new_nonce (16 bytes)` +- 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 (new client) +### Register ``` Client Host @@ -170,23 +186,32 @@ Client Host |---------------------------------------->| | | | Register { | - | Version, ClientNonce, | - | PublicKeys (serialized PublicKeyBundle), - | Signature, [PqSignature] | + | Version, | (unsigned hello) + | PublicKeys (serialized PublicKeyBundle) | } | |---------------------------------------->| - | | extract PublicKeyBundle from frame - | | verify Ed25519 (and optional ML-DSA) sig + | | 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, ClientNonce(echoed), | - | Id, Timestamp(new_nonce), | + | Connected=true, Id(new_id), | + | ClientNonce(echoed), | | Signature, [PqSignature] | | } | |<----------------------------------------| ``` -The client signature payload is: `version_string || client_nonce (16 bytes) || public_key_bytes` +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` diff --git a/README.md b/README.md index fa3bbca..4e44331 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ MTP is a modular transport protocol built on QUIC. It provides version-negotiable type maps, a binary codec, cryptographic primitives (classical and post-quantum), and host/client connection management with mutual authentication. -There are Area specific docs when working with seperate concerns for the [Native-Client](./NATIVE-CLIENT.md), [WASM-Client](./WASM-CLIENT.md) & [Host](./NATIVE-HOST.md) +See the area-specific docs for [Native Client](./NATIVE-CLIENT.md), [WASM Client](./WASM-CLIENT.md), and [Host](./NATIVE-HOST.md) ## Getting Started diff --git a/WASM-CLIENT.md b/WASM-CLIENT.md index b27b45e..f42c033 100644 --- a/WASM-CLIENT.md +++ b/WASM-CLIENT.md @@ -97,7 +97,7 @@ await client.connect(config); Sends an `Identification` frame with the protocol version and client ID. The host may accept or reject. No cryptographic handshake occurs. -### Authenticated Login (existing client ID) +### Authenticated Login ```typescript const confirmedId = await client.auth_connect( @@ -108,10 +108,14 @@ const confirmedId = await client.auth_connect( ); ``` -Exchange: client sends a signed `Identification` frame, the host verifies it and -responds with a signed `IdentificationResponse`. Returns the confirmed client ID. +Exchange (challenge-response): the client sends an unsigned `Identification` +hello, the host replies with a signed `Challenge` carrying a fresh +`server_challenge`, the client signs that challenge in a `ChallengeResponse`, and +the host verifies it and replies with a signed `IdentificationResponse`. Signing +over the host-issued challenge is what prevents a captured proof from being +replayed on another connection. Returns the confirmed client ID. -### Registration (new client) +### Registration ```typescript const newId = await client.auth_register( @@ -121,8 +125,10 @@ const newId = await client.auth_register( ); ``` -Exchange: client sends a signed `Register` frame with public keys, the host -assigns a new ID and responds with a signed `RegisterResponse`. Returns the +Exchange (challenge-response): the client sends an unsigned `Register` hello with +its public keys, the host replies with a signed `Challenge`, the client signs it +(binding the public-key bundle) in a `ChallengeResponse`, and the host verifies +it, assigns a new ID, and responds with a signed `RegisterResponse`. Returns the newly assigned client ID. ## Sending and Receiving Messages diff --git a/client/src/lib.rs b/client/src/lib.rs index 9226838..ea42ddb 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -22,7 +22,7 @@ pub struct ClientConfig { pub client_id: u64, } -// Established MTP connection with a single negotiated version. +/* Established MTP connection with a single negotiated version. */ pub struct MTPConnection { pub version: Version, pub sender: Sender, @@ -78,6 +78,113 @@ impl MTPClient { } /* ===== Authentication ===== */ + +/* + * Verify the host's signature over the challenge it issued (step 2). + * + * `id` is the client id for a login, or `0` for a registration (the host binds + * `0` since no id has been assigned yet). The Ed25519 signature is mandatory; + * the ML-DSA signature is checked only when the host included one. + */ +#[cfg(feature = "crypto")] +fn verify_host_challenge( + challenge: &CommunicationValue, + tm: &mtp_codec::TypeMap, + host_pk: &mtp_crypto::PublicKeyBundle, + id: u64, + server_challenge: u128, +) -> Result<(), CommunicationError> { + use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa}; + + let sig = match challenge.get_data(DataType::Signature.to_id(tm)) { + DataValue::Bytes(b) => b.clone(), + _ => { + return Err(CommunicationError::AuthenticationFailed( + "Missing host challenge signature".into(), + )); + } + }; + let pq_sig = match challenge.get_data(DataType::PqSignature.to_id(tm)) { + DataValue::Bytes(b) => b.clone(), + _ => vec![], + }; + + let payload = auth::challenge_payload(id, server_challenge); + verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig).map_err(|_| { + CommunicationError::AuthenticationFailed("Host challenge signature invalid".into()) + })?; + if !pq_sig.is_empty() && verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig).is_err() { + return Err(CommunicationError::AuthenticationFailed( + "Host challenge PQ signature invalid".into(), + )); + } + Ok(()) +} + +/* + * Verify the host's final confirmation (step 4): the echoed `client_nonce` and + * the host signature over the handshake transcript. + */ +#[cfg(feature = "crypto")] +fn verify_host_final( + response: &CommunicationValue, + tm: &mtp_codec::TypeMap, + host_pk: &mtp_crypto::PublicKeyBundle, + id: u64, + client_nonce: u128, + server_challenge: u128, +) -> Result<(), CommunicationError> { + use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa}; + + match response.get_data(DataType::ClientNonce.to_id(tm)) { + DataValue::UnsignedNumber(n) if *n == client_nonce => {} + _ => { + return Err(CommunicationError::AuthenticationFailed( + "Nonce mismatch".into(), + )); + } + } + + let sig = match response.get_data(DataType::Signature.to_id(tm)) { + DataValue::Bytes(b) => b.clone(), + _ => { + return Err(CommunicationError::AuthenticationFailed( + "Missing signature".into(), + )); + } + }; + let pq_sig = match response.get_data(DataType::PqSignature.to_id(tm)) { + DataValue::Bytes(b) => b.clone(), + _ => vec![], + }; + + let payload = auth::host_final_payload(id, client_nonce, server_challenge); + verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig) + .map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?; + if !pq_sig.is_empty() && verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig).is_err() { + return Err(CommunicationError::AuthenticationFailed( + "Host PQ signature invalid".into(), + )); + } + Ok(()) +} + +/* Interpret the host's `Connected` flag. */ +#[cfg(feature = "crypto")] +fn check_connected( + response: &CommunicationValue, + tm: &mtp_codec::TypeMap, + reject_msg: &str, +) -> Result<(), CommunicationError> { + match response.get_data(DataType::Connected.to_id(tm)) { + DataValue::BoolTrue => Ok(()), + DataValue::BoolFalse => Err(CommunicationError::AuthenticationFailed(reject_msg.into())), + _ => Err(CommunicationError::AuthenticationFailed( + "Invalid response".into(), + )), + } +} + #[cfg(feature = "crypto")] impl MTPClient { pub async fn auth_connect( @@ -85,59 +192,82 @@ impl MTPClient { keys: &mtp_crypto::Keyring, host_public_key_bundle: &mtp_crypto::PublicKeyBundle, ) -> Result { - use mtp_crypto::{ - Ed25519Signer, MlDsaSigner, SignatureScheme, verify_ed25519, verify_ml_dsa, - }; + use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth}; let (sender, receiver) = mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; - // 1. Build and send Identification message immediately (no greeting) - let client_nonce: u128 = rand::random(); + let tm = mtp_codec::TypeMap::latest(); let version_str = format!("{}", PROTOCOL_VERSION); - let mut sig_payload = Vec::new(); - sig_payload.extend_from_slice(version_str.as_bytes()); - sig_payload.extend_from_slice(&config.client_id.to_be_bytes()); - sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - - let signer = Ed25519Signer::new(&keys.sig_cl_secret_key) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - - /* ===== Signature ===== */ - let signature = signer - .sign(&sig_payload) - .map_err(|e| CommunicationError::Other(e.to_string()))?; - - let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification) - .add_typed_default(DataType::Version, DataValue::Str(version_str)) + // 1. Send the unsigned Identification hello (version + claimed id). + let 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), - ) + ); + sender.send(&ident).await?; + + // 2. Receive and verify the host's challenge. + let challenge = receiver.receive().await?; + let expected = mtp_codec::CommunicationType::Challenge.to_id(&tm); + if challenge.get_type() != expected { + return Err(unexpected_response_type_error( + "auth_connect challenge", + expected, + &challenge, + )); + } + let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { + DataValue::UnsignedNumber(n) => *n, + _ => { + return Err(CommunicationError::AuthenticationFailed( + "Missing server challenge".into(), + )); + } + }; + verify_host_challenge( + &challenge, + &tm, + host_public_key_bundle, + config.client_id, + server_challenge, + )?; + + // 3. Sign the host's challenge and send the proof. + let client_nonce: u128 = rand::random(); + let proof_payload = auth::login_proof_payload( + &version_str, + config.client_id, + server_challenge, + client_nonce, + ); + + let signer = Ed25519Signer::new(&keys.sig_cl_secret_key) + .map_err(|e| CommunicationError::Other(e.to_string()))?; + let signature = signer + .sign(&proof_payload) + .map_err(|e| CommunicationError::Other(e.to_string()))?; + + let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse) .add_typed_default( DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce), ) .add_typed_default(DataType::Signature, DataValue::Bytes(signature)); - if !keys.sig_pq_secret_key.as_bytes().is_empty() { let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key) .map_err(|e| CommunicationError::Other(e.to_string()))?; let pq_signature = pq_signer - .sign(&sig_payload) + .sign(&proof_payload) .map_err(|e| CommunicationError::Other(e.to_string()))?; - ident = ident.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); + proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); } - /* ===== End Signature ===== */ + sender.send(&proof).await?; - sender.send(&ident).await?; - - // 2. Receive host response (single message) + // 4. Receive and verify the host's final confirmation. let response = receiver.receive().await?; - - let tm = mtp_codec::TypeMap::latest(); - let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm); if response.get_type() != expected_type { return Err(unexpected_response_type_error( @@ -146,81 +276,15 @@ impl MTPClient { &response, )); } - - let connected = response.get_data(DataType::Connected.to_id(&tm)); - match connected { - DataValue::BoolTrue => {} - DataValue::BoolFalse => { - return Err(CommunicationError::AuthenticationFailed( - "Server rejected authentication".into(), - )); - } - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Invalid response".into(), - )); - } - } - - let echo_nonce = response.get_data(DataType::ClientNonce.to_id(&tm)); - match echo_nonce { - DataValue::UnsignedNumber(n) if *n == client_nonce as u128 => {} - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Nonce mismatch".into(), - )); - } - } - - let host_new_nonce = match response.get_data(DataType::Timestamp.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Missing new nonce".into(), - )); - } - }; - - let host_sig = match response.get_data(DataType::Signature.to_id(&tm)) { - DataValue::Bytes(b) => b.clone(), - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Missing signature".into(), - )); - } - }; - - let host_pq_sig = match response.get_data(DataType::PqSignature.to_id(&tm)) { - DataValue::Bytes(b) => b.clone(), - _ => vec![], - }; - - let mut host_sig_payload = Vec::new(); - host_sig_payload.push(0x01); - host_sig_payload.extend_from_slice(&config.client_id.to_be_bytes()); - host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - host_sig_payload.extend_from_slice(&host_new_nonce.to_be_bytes()); - - /* ===== Signature ===== */ - verify_ed25519( - &host_public_key_bundle.sig_cl_public_key, - &host_sig_payload, - &host_sig, - ) - .map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?; - if !host_pq_sig.is_empty() - && verify_ml_dsa( - &host_public_key_bundle.sig_pq_public_key, - &host_sig_payload, - &host_pq_sig, - ) - .is_err() - { - return Err(CommunicationError::AuthenticationFailed( - "Host PQ signature invalid".into(), - )); - } - /* ===== End Signature ===== */ + check_connected(&response, &tm, "Server rejected authentication")?; + verify_host_final( + &response, + &tm, + host_public_key_bundle, + config.client_id, + client_nonce, + server_challenge, + )?; Ok(MTPConnection { version: PROTOCOL_VERSION, @@ -236,59 +300,72 @@ impl MTPClient { keys: &mtp_crypto::Keyring, host_public_key_bundle: &mtp_crypto::PublicKeyBundle, ) -> Result { - use mtp_crypto::{ - Ed25519Signer, MlDsaSigner, SignatureScheme, verify_ed25519, verify_ml_dsa, - }; + use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth}; let (sender, receiver) = mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; - // 1. Build and send Register message immediately - let client_nonce: u128 = rand::random(); + let tm = mtp_codec::TypeMap::latest(); let version_str = format!("{}", PROTOCOL_VERSION); let pk_bundle = keys.public_key_bundle(); let pk_bytes = pk_bundle.as_bytes(); - let mut sig_payload = Vec::new(); - sig_payload.extend_from_slice(version_str.as_bytes()); - sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - sig_payload.extend_from_slice(&pk_bytes); + // 1. Send the unsigned Register hello (version + public-key bundle). + let 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())); + sender.send(®ister).await?; + + // 2. Receive and verify the host's challenge (register binds id = 0). + let challenge = receiver.receive().await?; + let expected = mtp_codec::CommunicationType::Challenge.to_id(&tm); + if challenge.get_type() != expected { + return Err(unexpected_response_type_error( + "auth_register challenge", + expected, + &challenge, + )); + } + let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { + DataValue::UnsignedNumber(n) => *n, + _ => { + return Err(CommunicationError::AuthenticationFailed( + "Missing server challenge".into(), + )); + } + }; + verify_host_challenge(&challenge, &tm, host_public_key_bundle, 0, server_challenge)?; + + // 3. Sign the host's challenge over the bundle and send the proof. + let client_nonce: u128 = rand::random(); + let proof_payload = + auth::register_proof_payload(&version_str, &pk_bytes, server_challenge, client_nonce); let signer = Ed25519Signer::new(&keys.sig_cl_secret_key) .map_err(|e| CommunicationError::Other(e.to_string()))?; - - /* ===== Signature ===== */ let signature = signer - .sign(&sig_payload) + .sign(&proof_payload) .map_err(|e| CommunicationError::Other(e.to_string()))?; - let mut register = CommunicationValue::new(mtp_codec::CommunicationType::Register) - .add_typed_default(DataType::Version, DataValue::Str(version_str)) + let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse) .add_typed_default( DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce), ) - .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes)) .add_typed_default(DataType::Signature, DataValue::Bytes(signature)); - if !keys.sig_pq_secret_key.as_bytes().is_empty() { let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key) .map_err(|e| CommunicationError::Other(e.to_string()))?; let pq_signature = pq_signer - .sign(&sig_payload) + .sign(&proof_payload) .map_err(|e| CommunicationError::Other(e.to_string()))?; - register = - register.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); + proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature)); } - /* ===== End Signature ===== */ + sender.send(&proof).await?; - sender.send(®ister).await?; - - // 2. Receive host response (single message) + // 4. Receive the host's final confirmation; extract the assigned id and + // verify the host signature binds to it. let response = receiver.receive().await?; - - let tm = mtp_codec::TypeMap::latest(); - let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm); if response.get_type() != expected_type { return Err(unexpected_response_type_error( @@ -297,97 +374,30 @@ impl MTPClient { &response, )); } - - let connected = response.get_data(DataType::Connected.to_id(&tm)); - match connected { - DataValue::BoolTrue => {} - DataValue::BoolFalse => { - return Err(CommunicationError::AuthenticationFailed( - "Server rejected registration".into(), - )); - } - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Invalid response".into(), - )); - } - } - + check_connected(&response, &tm, "Server rejected registration")?; let assigned_id = match response.get_data(DataType::Id.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, + DataValue::UnsignedNumber(n) => *n as u64, _ => { return Err(CommunicationError::AuthenticationFailed( "Missing assigned ID".into(), )); } }; - - let echo_nonce = response.get_data(DataType::ClientNonce.to_id(&tm)); - match echo_nonce { - DataValue::UnsignedNumber(n) if *n == client_nonce as u128 => {} - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Nonce mismatch".into(), - )); - } - } - - let host_new_nonce = match response.get_data(DataType::Timestamp.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Missing new nonce".into(), - )); - } - }; - - let host_sig = match response.get_data(DataType::Signature.to_id(&tm)) { - DataValue::Bytes(b) => b.clone(), - _ => { - return Err(CommunicationError::AuthenticationFailed( - "Missing signature".into(), - )); - } - }; - - let host_pq_sig = match response.get_data(DataType::PqSignature.to_id(&tm)) { - DataValue::Bytes(b) => b.clone(), - _ => vec![], - }; - - let mut host_sig_payload = Vec::new(); - host_sig_payload.push(0x01); - host_sig_payload.extend_from_slice(&(assigned_id as u64).to_be_bytes()); - host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - host_sig_payload.extend_from_slice(&host_new_nonce.to_be_bytes()); - - /* ===== Signature ===== */ - verify_ed25519( - &host_public_key_bundle.sig_cl_public_key, - &host_sig_payload, - &host_sig, - ) - .map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?; - if !host_pq_sig.is_empty() - && verify_ml_dsa( - &host_public_key_bundle.sig_pq_public_key, - &host_sig_payload, - &host_pq_sig, - ) - .is_err() - { - return Err(CommunicationError::AuthenticationFailed( - "Host PQ signature invalid".into(), - )); - } - /* ===== End Signature ===== */ + verify_host_final( + &response, + &tm, + host_public_key_bundle, + assigned_id, + client_nonce, + server_challenge, + )?; Ok(MTPConnection { version: PROTOCOL_VERSION, sender, receiver, auth_state: AuthState::Authenticated, - client_id: assigned_id as u64, + client_id: assigned_id, }) } } diff --git a/codec/src/communication_value.rs b/codec/src/communication_value.rs index 7dbfec0..636fe2b 100644 --- a/codec/src/communication_value.rs +++ b/codec/src/communication_value.rs @@ -345,7 +345,7 @@ impl CommunicationValue { let data = if is_encrypted { let mut map = BTreeMap::new(); map.insert( - DataTypeId(0), + DataType::Version.to_id(&TypeMap::latest()), DataValue::EncryptedContainer(data_bytes.to_vec()), ); map @@ -582,7 +582,7 @@ mod tests { assert_eq!(total_len as usize + 4, bytes.len()); let typ = c.read_u16::().expect("read type"); - assert_eq!(typ, 1); + assert_eq!(typ, 12); let flags = c.read_u8().expect("read flags"); assert_eq!(flags & 0b0000_0111, 0); @@ -602,7 +602,7 @@ mod tests { assert_eq!(total_len as usize + 4, bytes.len()); let typ = c.read_u16::().expect("read type"); - assert_eq!(typ, 2); + assert_eq!(typ, 13); let flags = c.read_u8().expect("read flags"); assert_eq!(flags & 0b0000_0111, 0b0000_0111); @@ -621,15 +621,16 @@ mod tests { #[test] fn test_roundtrip_complex() { + let tm = TypeMap::latest(); let cv = CommunicationValue::new(CommunicationType::Disconnect) .with_id(1234) .with_sender(111) .with_receiver(222) - .add_data(DataTypeId(1), DataValue::Str("alice".to_string())) - .add_data(DataTypeId(2), DataValue::SignedNumber(42)) - .add_data(DataTypeId(3), DataValue::BoolTrue) - .add_data( - DataTypeId(4), + .add_typed_default(DataType::Id, DataValue::Str("alice".to_string())) + .add_typed_default(DataType::ClientNonce, DataValue::SignedNumber(42)) + .add_typed_default(DataType::ServerNonce, DataValue::BoolTrue) + .add_typed_default( + DataType::PublicKeys, DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]), ); @@ -638,13 +639,13 @@ mod tests { assert_eq!(decoded.get_id(), 1234); assert_eq!(decoded.get_sender(), 111); assert_eq!(decoded.get_receiver(), 222); - assert_eq!(decoded.get_type(), CommunicationTypeId(3)); + assert_eq!(decoded.get_type(), CommunicationType::Disconnect.to_id(&tm)); assert_eq!( - decoded.get_data(DataTypeId(1)), + decoded.get_data(DataType::Id.to_id(&tm)), &DataValue::Str("alice".to_string()) ); assert_eq!( - decoded.get_data(DataTypeId(2)), + decoded.get_data(DataType::ClientNonce.to_id(&tm)), &DataValue::SignedNumber(42) ); } @@ -668,7 +669,7 @@ mod tests { .with_id(7) .with_sender(1) .with_receiver(2) - .add_data(DataTypeId(6), DataValue::UnsignedNumber(42)); + .add_typed_default(DataType::PqSignature, DataValue::UnsignedNumber(42)); assert!(cv.sign_frame(SigAlgorithm::ED25519, &signer).is_some()); @@ -691,7 +692,7 @@ mod tests { let (_, other_sk, _) = Ed25519Signer::generate(); let mut cv = CommunicationValue::new(CommunicationType::Ping) - .add_data(DataTypeId(6), DataValue::UnsignedNumber(42)); + .add_typed_default(DataType::PqSignature, DataValue::UnsignedNumber(42)); assert!(cv.sign_frame(SigAlgorithm::ED25519, &signer).is_some()); let wrong = Ed25519Signer::new(&other_sk).unwrap(); diff --git a/codec/src/data_value.rs b/codec/src/data_value.rs index 9c2c21b..886fe71 100644 --- a/codec/src/data_value.rs +++ b/codec/src/data_value.rs @@ -9,6 +9,9 @@ use std::io::Cursor; use mtp_common::CodecError; use mtp_type_map::DataTypeId; +#[cfg(test)] +use mtp_type_map::{DataType, TypeMap}; + #[cfg(feature = "crypto")] use mtp_crypto::{EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm, SignatureScheme}; @@ -151,11 +154,13 @@ impl DataValue { const KIND_NULL: u8 = 0xFF; - /// Smallest possible encoded entry, used to cap pre-reservation when - /// decoding containers/arrays so a small frame cannot force a huge - /// allocation from an attacker-controlled count. A bool/null entry in a - /// container is 3 bytes (1 kind + 2 key); a bare value in an array is 1 - /// byte, so 1 is the safe lower bound shared by both. + /* + * Smallest possible encoded entry, used to cap pre-reservation when + * decoding containers/arrays so a small frame cannot force a huge + * allocation from an attacker-controlled count. A bool/null entry in a + * container is 3 bytes (1 kind + 2 key); a bare value in an array is 1 + * byte, so 1 is the safe lower bound shared by both. + */ const MIN_ENTRY_BYTES: usize = 1; pub fn container_from_map(map: &BTreeMap) -> DataValue { @@ -954,8 +959,6 @@ impl Hash for DataValue { mod tests { use super::*; - /// Only Container and Array can be top-level serialized forms. - /// Scalars must be tested inside a container. fn container_roundtrip(values: Vec<(DataTypeId, DataValue)>) { let dv = DataValue::Container(values.clone()); let bytes = dv.to_bytes().expect("encode failed"); @@ -972,9 +975,10 @@ mod tests { #[test] fn test_bool_in_container() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::BoolTrue), - (DataTypeId(2), DataValue::BoolFalse), + (DataType::Id.to_id(&tm), DataValue::BoolTrue), + (DataType::ClientNonce.to_id(&tm), DataValue::BoolFalse), ]); } @@ -995,54 +999,60 @@ mod tests { #[test] fn test_signed_number_in_container() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::SignedNumber(0)), - (DataTypeId(2), DataValue::SignedNumber(42)), - (DataTypeId(3), DataValue::SignedNumber(-42)), - (DataTypeId(4), DataValue::SignedNumber(i128::MAX)), - (DataTypeId(5), DataValue::SignedNumber(i128::MIN)), + (DataType::Version.to_id(&tm), DataValue::SignedNumber(0)), + (DataType::Id.to_id(&tm), DataValue::SignedNumber(42)), + (DataType::ClientNonce.to_id(&tm), DataValue::SignedNumber(-42)), + (DataType::ServerNonce.to_id(&tm), DataValue::SignedNumber(i128::MAX)), + (DataType::PublicKeys.to_id(&tm), DataValue::SignedNumber(i128::MIN)), ]); } #[test] fn test_unsigned_number_in_container() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::UnsignedNumber(0)), - (DataTypeId(2), DataValue::UnsignedNumber(42)), - (DataTypeId(3), DataValue::UnsignedNumber(u128::MAX)), + (DataType::Version.to_id(&tm), DataValue::UnsignedNumber(0)), + (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)), + (DataType::ClientNonce.to_id(&tm), DataValue::UnsignedNumber(u128::MAX)), ]); } #[test] fn test_float_in_container() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::Float(0, 0)), - (DataTypeId(2), DataValue::Float(2, 12345)), - (DataTypeId(3), DataValue::Float(255, 4294967295)), + (DataType::Version.to_id(&tm), DataValue::Float(0, 0)), + (DataType::Id.to_id(&tm), DataValue::Float(2, 12345)), + (DataType::ClientNonce.to_id(&tm), DataValue::Float(255, 4294967295)), ]); } #[test] fn test_str_in_container() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::Str(String::new())), - (DataTypeId(2), DataValue::Str("hello".to_string())), - (DataTypeId(3), DataValue::Str("a".repeat(1000))), + (DataType::Version.to_id(&tm), DataValue::Str(String::new())), + (DataType::Id.to_id(&tm), DataValue::Str("hello".to_string())), + (DataType::ClientNonce.to_id(&tm), DataValue::Str("a".repeat(1000))), ]); } #[test] fn test_bytes_in_container() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::Bytes(vec![])), - (DataTypeId(2), DataValue::Bytes(vec![0x00, 0xFF, 0xAB])), - (DataTypeId(3), DataValue::Bytes(vec![0x42; 100])), + (DataType::Version.to_id(&tm), DataValue::Bytes(vec![])), + (DataType::Id.to_id(&tm), DataValue::Bytes(vec![0x00, 0xFF, 0xAB])), + (DataType::ClientNonce.to_id(&tm), DataValue::Bytes(vec![0x42; 100])), ]); } #[test] fn test_null_in_container() { - container_roundtrip(vec![(DataTypeId(1), DataValue::Null)]); + let tm = TypeMap::latest(); + container_roundtrip(vec![(DataType::Version.to_id(&tm), DataValue::Null)]); } #[test] @@ -1070,24 +1080,26 @@ mod tests { #[test] fn test_container_mixed_roundtrip() { + let tm = TypeMap::latest(); container_roundtrip(vec![ - (DataTypeId(1), DataValue::BoolTrue), - (DataTypeId(2), DataValue::SignedNumber(-100)), - (DataTypeId(3), DataValue::Str("test".to_string())), - (DataTypeId(4), DataValue::UnsignedNumber(u128::MAX)), - (DataTypeId(5), DataValue::Null), + (DataType::Version.to_id(&tm), DataValue::BoolTrue), + (DataType::Id.to_id(&tm), DataValue::SignedNumber(-100)), + (DataType::ClientNonce.to_id(&tm), DataValue::Str("test".to_string())), + (DataType::ServerNonce.to_id(&tm), DataValue::UnsignedNumber(u128::MAX)), + (DataType::PublicKeys.to_id(&tm), DataValue::Null), ]); } #[test] fn test_container_nested_roundtrip() { + let tm = TypeMap::latest(); container_roundtrip(vec![ ( - DataTypeId(1), - DataValue::Container(vec![(DataTypeId(10), DataValue::BoolTrue)]), + DataType::Version.to_id(&tm), + DataValue::Container(vec![(DataType::Error.to_id(&tm), DataValue::BoolTrue)]), ), ( - DataTypeId(2), + DataType::Id.to_id(&tm), DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]), ), ]); @@ -1095,8 +1107,9 @@ mod tests { #[test] fn test_container_base64_roundtrip() { + let tm = TypeMap::latest(); let dv = DataValue::Container(vec![( - DataTypeId(7), + DataType::Description.to_id(&tm), DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]), )]); let b64 = dv.to_base64().expect("encode failed"); @@ -1126,28 +1139,29 @@ mod tests { #[test] fn test_as_accessors() { + let tm = TypeMap::latest(); let dv = DataValue::Container(vec![ - (DataTypeId(1), DataValue::Str("alice".to_string())), - (DataTypeId(2), DataValue::SignedNumber(42)), - (DataTypeId(3), DataValue::Bytes(vec![0x01, 0x02])), - (DataTypeId(4), DataValue::Array(vec![DataValue::BoolTrue])), + (DataType::Version.to_id(&tm), DataValue::Str("alice".to_string())), + (DataType::Id.to_id(&tm), DataValue::SignedNumber(42)), + (DataType::ClientNonce.to_id(&tm), DataValue::Bytes(vec![0x01, 0x02])), + (DataType::ServerNonce.to_id(&tm), DataValue::Array(vec![DataValue::BoolTrue])), ]); let map = dv.as_map().expect("should be a container"); assert_eq!( - map.get(&DataTypeId(1)).and_then(|v| v.as_str()), + map.get(&DataType::Version.to_id(&tm)).and_then(|v| v.as_str()), Some("alice") ); assert_eq!( - map.get(&DataTypeId(2)).and_then(|v| v.as_signed_number()), + map.get(&DataType::Id.to_id(&tm)).and_then(|v| v.as_signed_number()), Some(42) ); assert_eq!( - map.get(&DataTypeId(3)).and_then(|v| v.as_bytes()), + map.get(&DataType::ClientNonce.to_id(&tm)).and_then(|v| v.as_bytes()), Some(vec![0x01, 0x02]) ); assert_eq!( - map.get(&DataTypeId(4)).and_then(|v| v.as_array()), + map.get(&DataType::ServerNonce.to_id(&tm)).and_then(|v| v.as_array()), Some(vec![DataValue::BoolTrue]) ); } @@ -1168,9 +1182,10 @@ mod tests { #[test] fn test_container_from_map() { + let tm = TypeMap::latest(); let mut map = BTreeMap::new(); - map.insert(DataTypeId(1), DataValue::BoolTrue); - map.insert(DataTypeId(2), DataValue::SignedNumber(99)); + map.insert(DataType::Version.to_id(&tm), DataValue::BoolTrue); + map.insert(DataType::Id.to_id(&tm), DataValue::SignedNumber(99)); let dv = DataValue::container_from_map(&map); let container = dv.as_container().expect("should be container"); assert_eq!(container.len(), 2); @@ -1190,7 +1205,8 @@ mod tests { #[test] fn test_truncated_container_rejected() { - let dv = DataValue::Container(vec![(DataTypeId(1), DataValue::Str("hello".to_string()))]); + let tm = TypeMap::latest(); + let dv = DataValue::Container(vec![(DataType::Version.to_id(&tm), DataValue::Str("hello".to_string()))]); let bytes = dv.to_bytes().expect("encode failed"); // Truncate to fewer than 2 bytes so neither container nor array can be read assert!(DataValue::from_bytes(&bytes[..1]).is_none()); @@ -1246,9 +1262,10 @@ mod tests { #[test] fn test_container_display() { + let tm = TypeMap::latest(); let dv = DataValue::Container(vec![ - (DataTypeId(3), DataValue::Str("v2.0".to_string())), - (DataTypeId(6), DataValue::UnsignedNumber(42)), + (DataType::ServerNonce.to_id(&tm), DataValue::Str("v2.0".to_string())), + (DataType::PqSignature.to_id(&tm), DataValue::UnsignedNumber(42)), ]); let s = format!("{}", dv); assert!(s.contains("3:")); @@ -1268,12 +1285,13 @@ mod tests { #[test] fn test_encrypt_decrypt_container_roundtrip() { use mtp_crypto::{EncryptionType, Keyring}; + let tm = TypeMap::latest(); let keyring = Keyring::generate(); let bundle = keyring.public_key_bundle(); let mut dv = DataValue::Container(vec![ - (DataTypeId(1), DataValue::Str("secret".to_string())), - (DataTypeId(2), DataValue::UnsignedNumber(42)), + (DataType::Version.to_id(&tm), DataValue::Str("secret".to_string())), + (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)), ]); assert!( @@ -1293,11 +1311,12 @@ mod tests { #[test] fn test_encrypt_container_wrong_key_fails() { use mtp_crypto::{EncryptionType, Keyring}; + let tm = TypeMap::latest(); let keyring_a = Keyring::generate(); let keyring_b = Keyring::generate(); let mut dv = - DataValue::Container(vec![(DataTypeId(1), DataValue::Str("secret".to_string()))]); + DataValue::Container(vec![(DataType::Version.to_id(&tm), DataValue::Str("secret".to_string()))]); assert!( dv.encrypt_container( @@ -1314,10 +1333,11 @@ mod tests { #[test] fn test_encrypt_container_wrong_aad_fails() { use mtp_crypto::{EncryptionType, Keyring}; + let tm = TypeMap::latest(); let keyring = Keyring::generate(); let mut dv = - DataValue::Container(vec![(DataTypeId(1), DataValue::Str("secret".to_string()))]); + DataValue::Container(vec![(DataType::Version.to_id(&tm), DataValue::Str("secret".to_string()))]); assert!( dv.encrypt_container( @@ -1351,12 +1371,13 @@ mod tests { #[test] fn test_sign_verify_container_roundtrip() { use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, SigAlgorithm}; + let tm = TypeMap::latest(); let keyring = Keyring::generate(); let (signer, sk, _pk) = Ed25519Signer::generate(); let mut dv = DataValue::Container(vec![( - DataTypeId(1), + DataType::Version.to_id(&tm), DataValue::Str("signed data".to_string()), )]); @@ -1390,13 +1411,14 @@ mod tests { #[test] fn test_sign_container_wrong_key_fails() { use mtp_crypto::{Ed25519Signer, SigAlgorithm}; + let tm = TypeMap::latest(); let (signer, _, _) = Ed25519Signer::generate(); let (_, sk2, _) = Ed25519Signer::generate(); let wrong_verifier = Ed25519Signer::new(&sk2).unwrap(); let mut dv = DataValue::Container(vec![( - DataTypeId(1), + DataType::Version.to_id(&tm), DataValue::Str("signed data".to_string()), )]); diff --git a/common/src/lib.rs b/common/src/lib.rs index 05ac9f6..0a239ee 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -42,13 +42,11 @@ mod tests { } } -// =========================================================================== -// CommunicationError -// -// On native targets the full variant set (including quinn / wtransport -// wrappers) is available. On WASM only the transport-independent subset is -// compiled. -// =========================================================================== +/* CommunicationError + * + * On native targets the full variant set (including quinn / wtransport + * wrappers) is available. On WASM only the transport-independent subset is + * compiled. */ #[cfg(not(target_arch = "wasm32"))] #[derive(Debug, Error, Clone)] diff --git a/crypto/README.md b/crypto/README.md index 9c1c593..80e8f88 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -24,7 +24,7 @@ let pt = cipher.decrypt(&ct, b"aad")?; ## Signatures -### Ed25519 (classical) +### Ed25519 ```rust use mtp_crypto::{Ed25519Signer, SignatureScheme}; @@ -34,7 +34,7 @@ let sig = signer.sign(b"message")?; signer.verify(b"message", &sig)?; ``` -### ML-DSA-65 (post-quantum, requires `pqc`) +### ML-DSA-65 ```rust use mtp_crypto::{MlDsaSigner, SignatureScheme}; @@ -47,7 +47,7 @@ signer.verify(b"message", &sig)?; let signer = MlDsaSigner::new(&sk, &pk)?; ``` -### Dual signatures (requires `pqc`) +### Dual signatures ```rust use mtp_crypto::{sign_dual, DualSignature, Ed25519Signer, MlDsaSigner}; @@ -58,7 +58,7 @@ let dual = sign_dual(ed_signer.signing_key(), ml_signer.signing_key(), b"msg"); dual.verify(ed_signer.verifying_key(), ml_signer.verifying_key(), b"msg")?; ``` -## Hybrid KEM (requires `pqc`) +## Hybrid KEM X25519 + ML-KEM-768. 64-byte shared secret. Feed into HKDF before use. diff --git a/crypto/src/auth.rs b/crypto/src/auth.rs new file mode 100644 index 0000000..86186d6 --- /dev/null +++ b/crypto/src/auth.rs @@ -0,0 +1,128 @@ +/* + * Canonical signed payloads for the authentication handshake. + * + * Each payload starts with a unique [`domain`] tag so a signature for one step + * cannot be replayed as another. + * + * Handshake: + * Step 1. Client -> Host : Identification { version, id } (unsigned hello) + * Step 2. Host -> Client : Challenge { server_challenge, host_sig } host_sig over challenge_payload + * Step 3. Client -> Host : ChallengeResponse { client_nonce, sig } sig over login_proof_payload + * Step 4. Host -> Client : IdentificationResponse { connected, id, host_sig } host_sig over host_final_payload + */ + +/// Domain-separation tags — a distinct leading byte per signed context. +pub mod domain { + /// Host's signature over the challenge it issues (step 2). + pub const CHALLENGE: u8 = 0x10; + /// Client's authenticating proof for a login (step 3). + pub const LOGIN_PROOF: u8 = 0x11; + /// Client's authenticating proof for a registration (step 3). + pub const REGISTER_PROOF: u8 = 0x12; + /// Host's final confirmation signature (step 4). + pub const HOST_FINAL: u8 = 0x13; +} + +/* + * Host's challenge (step 2): binds `id` and `server_challenge` to prove host + * key possession before the client reveals its proof. + */ +pub fn challenge_payload(id: u64, server_challenge: u128) -> Vec { + let mut p = Vec::with_capacity(1 + 8 + 16); + p.push(domain::CHALLENGE); + p.extend_from_slice(&id.to_be_bytes()); + p.extend_from_slice(&server_challenge.to_be_bytes()); + p +} + +/* + * Client's login proof (step 3): binds version, id, server_challenge, client_nonce. + */ +pub fn login_proof_payload( + version: &str, + id: u64, + server_challenge: u128, + client_nonce: u128, +) -> Vec { + let mut p = Vec::with_capacity(1 + version.len() + 8 + 16 + 16); + p.push(domain::LOGIN_PROOF); + p.extend_from_slice(version.as_bytes()); + p.extend_from_slice(&id.to_be_bytes()); + p.extend_from_slice(&server_challenge.to_be_bytes()); + p.extend_from_slice(&client_nonce.to_be_bytes()); + p +} + +/* + * Client's registration proof (step 3): binds version, public_keys, server_challenge, client_nonce. + */ +pub fn register_proof_payload( + version: &str, + public_keys: &[u8], + server_challenge: u128, + client_nonce: u128, +) -> Vec { + let mut p = Vec::with_capacity(1 + version.len() + 16 + 16 + public_keys.len()); + p.push(domain::REGISTER_PROOF); + p.extend_from_slice(version.as_bytes()); + p.extend_from_slice(&server_challenge.to_be_bytes()); + p.extend_from_slice(&client_nonce.to_be_bytes()); + p.extend_from_slice(public_keys); + p +} + +/* + * Host's final confirmation (step 4): binds id, client_nonce, server_challenge + * to prove host liveness over a value the client chose. + */ +pub fn host_final_payload(id: u64, client_nonce: u128, server_challenge: u128) -> Vec { + let mut p = Vec::with_capacity(1 + 8 + 16 + 16); + p.push(domain::HOST_FINAL); + p.extend_from_slice(&id.to_be_bytes()); + p.extend_from_slice(&client_nonce.to_be_bytes()); + p.extend_from_slice(&server_challenge.to_be_bytes()); + p +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn domain_tags_are_distinct() { + let tags = [ + domain::CHALLENGE, + domain::LOGIN_PROOF, + domain::REGISTER_PROOF, + domain::HOST_FINAL, + ]; + for (i, a) in tags.iter().enumerate() { + for b in &tags[i + 1..] { + assert_ne!(a, b, "domain tags must be unique"); + } + } + } + + #[test] + fn login_and_register_proofs_never_collide() { + let login = login_proof_payload("1.0", 7, 1, 2); + let register = register_proof_payload("1.0", &7u64.to_be_bytes(), 1, 2); + assert_ne!(login, register); + assert_ne!(login[0], register[0]); + } + + #[test] + fn challenge_binds_id_and_value() { + assert_ne!(challenge_payload(1, 9), challenge_payload(2, 9)); + assert_ne!(challenge_payload(1, 9), challenge_payload(1, 8)); + assert_eq!(challenge_payload(1, 9)[0], domain::CHALLENGE); + } + + #[test] + fn proofs_bind_the_server_challenge() { + assert_ne!( + login_proof_payload("1.0", 3, 100, 200), + login_proof_payload("1.0", 3, 101, 200), + ); + } +} diff --git a/crypto/src/kdf.rs b/crypto/src/kdf.rs index 52baf4d..9b6f1a6 100644 --- a/crypto/src/kdf.rs +++ b/crypto/src/kdf.rs @@ -16,8 +16,10 @@ pub fn hkdf_expand( } pub fn hkdf_extract(ikm: &[u8], salt: &[u8]) -> [u8; 32] { - // Return the pseudo-random key (PRK) produced by HKDF-Extract directly. - // Extract cannot fail, so this avoids the panicking expand step entirely. + /* + * Return the pseudo-random key (PRK) produced by HKDF-Extract directly. + * Extract cannot fail, so this avoids the panicking expand step entirely. + */ let (prk, _) = Hkdf::::extract(Some(salt), ikm); let mut out = [0u8; 32]; out.copy_from_slice(&prk); diff --git a/crypto/src/keypair.rs b/crypto/src/keypair.rs index bf41106..adea54a 100644 --- a/crypto/src/keypair.rs +++ b/crypto/src/keypair.rs @@ -348,7 +348,7 @@ impl Keyring { .get(*offset..*offset + 2) .ok_or(CryptoError::InvalidKeyLength)? .try_into() - .unwrap(), + .expect("slice is 2 bytes, verified above"), ) as usize; *offset += 2; let key = bytes diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 1f91a75..d093a7a 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -1,4 +1,5 @@ pub mod aead; +pub mod auth; pub mod error; pub mod keypair; diff --git a/example-usage/client/src/messages.rs b/example-usage/client/src/messages.rs index f5b82a4..ca6ea50 100644 --- a/example-usage/client/src/messages.rs +++ b/example-usage/client/src/messages.rs @@ -1,6 +1,7 @@ use mtp::client::MTPConnection; -use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm}; +use mtp::type_map::TypeMap; pub fn build_demo_message( client_id: u64, @@ -12,26 +13,28 @@ pub fn build_demo_message( let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key) .expect("Ed25519 signer from keyring"); + let tm = TypeMap::latest(); + let inner_enc = DataValue::Container(vec![ - (DataTypeId(1), DataValue::Str("secret inner data".into())), - (DataTypeId(2), DataValue::UnsignedNumber(42)), + (DataType::Version.to_id(&tm), DataValue::Str("secret inner data".into())), + (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)), ]); let mut dv_enc = inner_enc; dv_enc.encrypt_container(enc_type, server_bundle, b"demo-aad"); let inner_sig = DataValue::Container(vec![ - (DataTypeId(1), DataValue::Str("signed by client".into())), - (DataTypeId(2), DataValue::UnsignedNumber(99)), + (DataType::Version.to_id(&tm), DataValue::Str("signed by client".into())), + (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(99)), ]); let mut dv_sig = inner_sig; dv_sig.sign_container(SigAlgorithm::ED25519, &signer); let inner_sec = DataValue::Container(vec![ ( - DataTypeId(1), + DataType::Version.to_id(&tm), DataValue::Str("signed+encrypted payload".into()), ), - (DataTypeId(2), DataValue::UnsignedNumber(7)), + (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(7)), ]); let mut dv_sec = inner_sec; dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, server_bundle, b"demo-aad"); diff --git a/host/src/lib.rs b/host/src/lib.rs index ddeeaed..600443b 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -6,7 +6,7 @@ use mtp_common::CommunicationError; use mtp_transport::{Policy, Receiver, Sender}; use std::net::IpAddr; -// Host configuration. +/* Host configuration. */ pub struct HostConfig { pub ip: IpAddr, pub port: u16, @@ -33,7 +33,7 @@ pub enum AuthState { Failed, } -// A connection that has completed version negotiation. +/* A connection that has completed version negotiation. */ pub struct MTPConnection { pub version: Version, pub codec: VersionedCodec, @@ -47,7 +47,7 @@ pub struct MTPConnection { pub client_public_key: Option, } -// High-level MTP host with built-in version negotiation. +/* High-level MTP host with built-in version negotiation. */ pub struct MTPHost { transport: mtp_transport::Host, registry: Registry, @@ -132,221 +132,235 @@ impl MTPHost { #[cfg(feature = "crypto")] impl MTPHost { + /* + * Mutually-authenticated handshake with a server-issued challenge. + * + * 1. C -> H : Identification { version, id } (or Register { version, public_keys }) + * 2. H -> C : Challenge { server_challenge, host_sig } + * 3. C -> H : ChallengeResponse { client_nonce, sig } + * 4. H -> C : IdentificationResponse / RegisterResponse { connected, id, sig } + * + * The client's authenticating signature (step 3) covers `server_challenge`, + * a fresh value generated here in step 2 and kept on this task's stack for + * the lifetime of the connection. It is therefore one-time per connection + * with no shared replay state, and a captured proof cannot be replayed on + * any other connection. + */ async fn accept_authenticated( &mut self, sender: Sender, receiver: Receiver, ) -> Option { use mtp_crypto::{ - Ed25519Signer, PublicKeyBundle, SignatureScheme, verify_ed25519, verify_ml_dsa, + Ed25519Signer, MlDsaSigner, PublicKeyBundle, SignatureScheme, auth, verify_ed25519, + verify_ml_dsa, }; - let tm = TypeMap::latest(); + // Flow-specific state resolved from the client's opening hello. + enum Flow { + Login { + id: u64, + bundle: PublicKeyBundle, + }, + Register { + bundle: PublicKeyBundle, + pk_bytes: Vec, + }, + } - // 1. Receive client message first (no host greeting) - let msg = receiver.receive().await.ok()?; - let version_str = match msg.get_data(DataType::Version.to_id(&tm)) { + let tm = 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]| -> Option<(Vec, Vec)> { + let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key).ok()?; + let sig = signer.sign(payload).ok()?; + 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, + ) + .ok()?; + pq.sign(payload).ok()? + } else { + Vec::new() + }; + Some((sig, pq_sig)) + }; + + // ===== Step 1: receive the client's unsigned hello ===== + let hello = receiver.receive().await.ok()?; + let version_str = match hello.get_data(DataType::Version.to_id(&tm)) { DataValue::Str(s) => s.clone(), _ => { sender.close(); return None; } }; - let client_version = Version::parse(&version_str)?; - let client_nonce = match msg.get_data(DataType::ClientNonce.to_id(&tm)) { + let (flow, response_type) = + if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) { + // LOGIN: look up the claimed user before issuing a challenge. + let cid = match hello.get_data(DataType::Id.to_id(&tm)) { + DataValue::UnsignedNumber(n) => *n as u64, + _ => { + sender.close(); + return None; + } + }; + let bundle = match (self.config.get_existing_user)(cid) { + Some(b) => b, + None => { + let rejection = CommunicationValue::new( + mtp_codec::CommunicationType::IdentificationResponse, + ) + .add_typed_default(DataType::Connected, DataValue::BoolFalse); + let _ = sender.send(&rejection).await; + sender.close(); + return None; + } + }; + ( + Flow::Login { id: cid, bundle }, + mtp_codec::CommunicationType::IdentificationResponse, + ) + } else if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) { + // REGISTER: the client presents the bundle it wants to register. + let bundle = match hello.get_data(DataType::PublicKeys.to_id(&tm)) { + DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).ok()?, + _ => { + sender.close(); + return None; + } + }; + let pk_bytes = bundle.as_bytes(); + ( + Flow::Register { bundle, pk_bytes }, + mtp_codec::CommunicationType::RegisterResponse, + ) + } else { + sender.close(); + return None; + }; + + // The id bound into the challenge (0 for register: none assigned yet). + let challenge_id = match &flow { + Flow::Login { id, .. } => *id, + Flow::Register { .. } => 0, + }; + + // ===== Step 2: issue a fresh, host-signed challenge ===== + let server_challenge: u128 = rand::random(); + let (chal_sig, chal_pq_sig) = + host_sign(&auth::challenge_payload(challenge_id, server_challenge))?; + + let mut challenge_msg = CommunicationValue::new(mtp_codec::CommunicationType::Challenge) + .add_typed_default( + DataType::ServerNonce, + DataValue::UnsignedNumber(server_challenge), + ) + .add_typed_default(DataType::Signature, DataValue::Bytes(chal_sig)); + if pq_enabled { + challenge_msg = challenge_msg + .add_typed_default(DataType::PqSignature, DataValue::Bytes(chal_pq_sig)); + } + sender.send(&challenge_msg).await.ok()?; + + // ===== Step 3: receive and verify the client's proof ===== + let proof = receiver.receive().await.ok()?; + if proof.get_type() != mtp_codec::CommunicationType::ChallengeResponse.to_id(&tm) { + sender.close(); + return None; + } + let client_nonce = match proof.get_data(DataType::ClientNonce.to_id(&tm)) { DataValue::UnsignedNumber(n) => *n, _ => { sender.close(); return None; } }; - - let sig_bytes = match msg.get_data(DataType::Signature.to_id(&tm)) { + let sig_bytes = match proof.get_data(DataType::Signature.to_id(&tm)) { DataValue::Bytes(b) => b.clone(), _ => { sender.close(); return None; } }; - - let pq_sig_bytes: Vec = match msg.get_data(DataType::PqSignature.to_id(&tm)) { + let pq_sig_bytes: Vec = match proof.get_data(DataType::PqSignature.to_id(&tm)) { DataValue::Bytes(b) => b.clone(), _ => vec![], }; - let (assigned_id, client_bundle, response_type) = if msg.get_type() - == mtp_codec::CommunicationType::Identification.to_id(&tm) - { - // LOGIN - let cid = match msg.get_data(DataType::Id.to_id(&tm)) { - DataValue::UnsignedNumber(n) => *n as u64, - _ => { - sender.close(); - return None; - } - }; - - let bundle = match (self.config.get_existing_user)(cid) { - Some(b) => b, - None => { - let rejection = CommunicationValue::new( - mtp_codec::CommunicationType::IdentificationResponse, - ) - .add_typed_default(DataType::Connected, DataValue::BoolFalse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ); - let _ = sender.send(&rejection).await; - sender.close(); - return None; - } - }; - - let mut sig_payload = Vec::new(); - sig_payload.extend_from_slice(version_str.as_bytes()); - sig_payload.extend_from_slice(&cid.to_be_bytes()); - sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - - /* ===== Signature ===== */ - if verify_ed25519(&bundle.sig_cl_public_key, &sig_payload, &sig_bytes).is_err() { - let rejection = - CommunicationValue::new(mtp_codec::CommunicationType::IdentificationResponse) - .add_typed_default(DataType::Connected, DataValue::BoolFalse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ); - let _ = sender.send(&rejection).await; - sender.close(); - return None; - } - if !pq_sig_bytes.is_empty() - && verify_ml_dsa(&bundle.sig_pq_public_key, &sig_payload, &pq_sig_bytes).is_err() - { - let rejection = - CommunicationValue::new(mtp_codec::CommunicationType::IdentificationResponse) - .add_typed_default(DataType::Connected, DataValue::BoolFalse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ); - let _ = sender.send(&rejection).await; - sender.close(); - return None; - } - /* ===== End Signature ===== */ - - ( - cid, + let (proof_payload, bundle) = match &flow { + Flow::Login { id, bundle } => ( + auth::login_proof_payload(&version_str, *id, server_challenge, client_nonce), bundle, - mtp_codec::CommunicationType::IdentificationResponse, - ) - } else if msg.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) { - // REGISTER - let bundle = match msg.get_data(DataType::PublicKeys.to_id(&tm)) { - DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).ok()?, - _ => { - sender.close(); - return None; - } - }; - let pk_bytes = bundle.as_bytes(); - - let mut sig_payload = Vec::new(); - sig_payload.extend_from_slice(version_str.as_bytes()); - sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - sig_payload.extend_from_slice(&pk_bytes); - - /* ===== Signature ===== */ - if verify_ed25519(&bundle.sig_cl_public_key, &sig_payload, &sig_bytes).is_err() { - let rejection = - CommunicationValue::new(mtp_codec::CommunicationType::RegisterResponse) - .add_typed_default(DataType::Connected, DataValue::BoolFalse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ); - let _ = sender.send(&rejection).await; - sender.close(); - return None; - } - if !pq_sig_bytes.is_empty() - && verify_ml_dsa(&bundle.sig_pq_public_key, &sig_payload, &pq_sig_bytes).is_err() - { - let rejection = - CommunicationValue::new(mtp_codec::CommunicationType::RegisterResponse) - .add_typed_default(DataType::Connected, DataValue::BoolFalse) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ); - let _ = sender.send(&rejection).await; - sender.close(); - return None; - } - /* ===== End Signature ===== */ - - let new_id = (self.config.complete_register)(bundle.clone()); - ( - new_id, + ), + Flow::Register { + bundle, pk_bytes, .. + } => ( + auth::register_proof_payload( + &version_str, + pk_bytes, + server_challenge, + client_nonce, + ), bundle, - mtp_codec::CommunicationType::RegisterResponse, - ) - } else { - sender.close(); - return None; + ), }; - // 2. Send success response (single host message) - let new_nonce: u128 = rand::random(); + let proof_ok = verify_ed25519(&bundle.sig_cl_public_key, &proof_payload, &sig_bytes) + .is_ok() + && (pq_sig_bytes.is_empty() + || verify_ml_dsa(&bundle.sig_pq_public_key, &proof_payload, &pq_sig_bytes).is_ok()); - let mut host_sig_payload = Vec::new(); - host_sig_payload.push(0x01); - host_sig_payload.extend_from_slice(&assigned_id.to_be_bytes()); - host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - host_sig_payload.extend_from_slice(&new_nonce.to_be_bytes()); + if !proof_ok { + let rejection = CommunicationValue::new(response_type) + .add_typed_default(DataType::Connected, DataValue::BoolFalse); + let _ = sender.send(&rejection).await; + sender.close(); + return None; + } - let host_signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key).ok()?; + // Proof verified: resolve the assigned id and retain the client's bundle. + let (assigned_id, client_bundle) = match flow { + Flow::Login { id, bundle } => (id, bundle), + Flow::Register { bundle, .. } => { + let new_id = (self.config.complete_register)(bundle.clone()); + (new_id, bundle) + } + }; - /* ===== Signature ===== */ - let host_sig = host_signer.sign(&host_sig_payload).ok()?; + // ===== Step 4: send the host's final confirmation ===== + let (host_sig, host_pq_sig) = host_sign(&auth::host_final_payload( + assigned_id, + client_nonce, + server_challenge, + ))?; let mut response = CommunicationValue::new(response_type) .add_typed_default(DataType::Connected, DataValue::BoolTrue) + .add_typed_default(DataType::Id, DataValue::UnsignedNumber(assigned_id as u128)) .add_typed_default( DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce), ) - .add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(new_nonce)) - .add_typed_default(DataType::Signature, DataValue::Bytes(host_sig)) - .add_typed_default(DataType::Id, DataValue::UnsignedNumber(assigned_id as u128)); - - if !self - .config - .host_keyring - .sig_pq_secret_key - .as_bytes() - .is_empty() - { - use mtp_crypto::MlDsaSigner; - let host_pq_signer = MlDsaSigner::new( - &self.config.host_keyring.sig_pq_secret_key, - &self.config.host_keyring.sig_pq_public_key, - ) - .ok()?; - let host_pq_sig = host_pq_signer.sign(&host_sig_payload).ok()?; + .add_typed_default(DataType::Signature, DataValue::Bytes(host_sig)); + if pq_enabled { response = response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig)); } - /* ===== End Signature ===== */ sender.send(&response).await.ok()?; sender.finish_stream().await.ok()?; - // 3. Version negotiation + // ===== Version negotiation ===== let negotiated = self.registry.negotiate(&[client_version])?; let codec = VersionedCodec::new(self.registry.clone()); diff --git a/transport/tests/integration.rs b/transport/tests/integration.rs index 82080be..d2ad8ac 100644 --- a/transport/tests/integration.rs +++ b/transport/tests/integration.rs @@ -1,6 +1,8 @@ use std::net::{IpAddr, Ipv4Addr}; +use mtp_codec::{CommunicationType, DataType}; use mtp_transport::{Policy, connect, host}; +use mtp_type_map::TypeMap; fn generate_self_signed_cert() -> (Vec, Vec) { let key_pair = rcgen::KeyPair::generate().unwrap(); @@ -51,22 +53,24 @@ async fn test_send_receive_roundtrip() { // Accept on host side let (host_tx, host_rx) = h.next().await.unwrap(); + let tm = TypeMap::latest(); + // Client sends a simple message - let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Ping).add_data( - mtp_codec::DataTypeId(6), + let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping).add_data( + DataType::PqSignature.to_id(&tm), mtp_codec::DataValue::UnsignedNumber(42), ); client_tx.send(&msg).await.unwrap(); // Host receives it let received = host_rx.receive().await.unwrap(); - assert_eq!(received.get_type(), mtp_codec::CommunicationTypeId(19)); // Ping - let val = received.get_data(mtp_codec::DataTypeId(6)).clone(); + assert_eq!(received.get_type(), CommunicationType::Ping.to_id(&tm)); + let val = received.get_data(DataType::PqSignature.to_id(&tm)).clone(); assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(42)); // Host sends a response - let resp = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Pong).add_data( - mtp_codec::DataTypeId(6), + let resp = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data( + DataType::PqSignature.to_id(&tm), mtp_codec::DataValue::UnsignedNumber(99), ); host_tx.send(&resp).await.unwrap(); @@ -75,9 +79,9 @@ async fn test_send_receive_roundtrip() { let client_received = client_rx.receive().await.unwrap(); assert_eq!( client_received.get_type(), - mtp_codec::CommunicationTypeId(20) - ); // Pong - let client_val = client_received.get_data(mtp_codec::DataTypeId(6)).clone(); + CommunicationType::Pong.to_id(&tm) + ); + let client_val = client_received.get_data(DataType::PqSignature.to_id(&tm)).clone(); assert_eq!(client_val, mtp_codec::DataValue::UnsignedNumber(99)); // Close both sides @@ -106,10 +110,12 @@ async fn test_concurrent_messages() { let (_host_tx, host_rx) = h.next().await.unwrap(); + let tm = TypeMap::latest(); + // Send 5 messages in sequence for i in 0..5u128 { - let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Ping).add_data( - mtp_codec::DataTypeId(6), + let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping).add_data( + DataType::PqSignature.to_id(&tm), mtp_codec::DataValue::UnsignedNumber(i), ); client_tx.send(&msg).await.unwrap(); @@ -118,14 +124,14 @@ async fn test_concurrent_messages() { // Receive all 5 in order for i in 0..5u128 { let received = host_rx.receive().await.unwrap(); - let val = received.get_data(mtp_codec::DataTypeId(6)).clone(); + let val = received.get_data(DataType::PqSignature.to_id(&tm)).clone(); assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(i)); } // Send 3 responses back for i in 0..3u128 { - let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Pong).add_data( - mtp_codec::DataTypeId(6), + let msg = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data( + DataType::PqSignature.to_id(&tm), mtp_codec::DataValue::UnsignedNumber(i * 10), ); client_tx.send(&msg).await.unwrap(); @@ -133,7 +139,7 @@ async fn test_concurrent_messages() { for i in 0..3u128 { let received = host_rx.receive().await.unwrap(); - let val = received.get_data(mtp_codec::DataTypeId(6)).clone(); + let val = received.get_data(DataType::PqSignature.to_id(&tm)).clone(); assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(i * 10)); } @@ -162,13 +168,14 @@ async fn test_close_detection() { let (_host_tx, host_rx) = h.next().await.unwrap(); // Send a message then close - let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Ping); + let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping); client_tx.send(&msg).await.unwrap(); client_tx.close(); // Host should still receive the message + let tm = TypeMap::latest(); let received = host_rx.receive().await.unwrap(); - assert_eq!(received.get_type(), mtp_codec::CommunicationTypeId(19)); // Ping + assert_eq!(received.get_type(), CommunicationType::Ping.to_id(&tm)); // Host should get an error or closed signal on next receive let result = host_rx.receive().await; @@ -234,7 +241,7 @@ async fn test_drop_receiver_keeps_sender_alive() { let (host_tx, host_rx) = h.next().await.unwrap(); // Client sends a message the host receives. - let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Ping); + let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping); client_tx.send(&msg).await.unwrap(); let _ = host_rx.receive().await.unwrap(); @@ -242,16 +249,18 @@ async fn test_drop_receiver_keeps_sender_alive() { // the same connection and must keep working. drop(host_rx); - let resp = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Pong).add_data( - mtp_codec::DataTypeId(6), + let tm = TypeMap::latest(); + + let resp = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data( + DataType::PqSignature.to_id(&tm), mtp_codec::DataValue::UnsignedNumber(7), ); host_tx.send(&resp).await.unwrap(); let got = client_rx.receive().await.unwrap(); - assert_eq!(got.get_type(), mtp_codec::CommunicationTypeId(20)); // Pong + assert_eq!(got.get_type(), CommunicationType::Pong.to_id(&tm)); assert_eq!( - got.get_data(mtp_codec::DataTypeId(6)).clone(), + got.get_data(DataType::PqSignature.to_id(&tm)).clone(), mtp_codec::DataValue::UnsignedNumber(7) ); diff --git a/type-map/build.rs b/type-map/build.rs old mode 100644 new mode 100755 index 06c1e9f..5d3d321 --- a/type-map/build.rs +++ b/type-map/build.rs @@ -25,139 +25,150 @@ struct ReservedEntry { const RESERVED_COMM_TYPES: &[ReservedEntry] = &[ ReservedEntry { - name: "Error", + name: "Identification", id: 0, }, ReservedEntry { - name: "ErrorParsing", + name: "IdentificationResponse", id: 1, }, ReservedEntry { - name: "ErrorBadVersion", + name: "Register", id: 2, }, ReservedEntry { - name: "Disconnect", + name: "RegisterResponse", id: 3, }, ReservedEntry { - name: "Redirect", + name: "Challenge", id: 4, }, ReservedEntry { - name: "Shutdown", + name: "ChallengeResponse", id: 5, }, ReservedEntry { - name: "BadRequest", + name: "Ping", id: 6, }, ReservedEntry { - name: "Unauthorized", + name: "Pong", id: 7, }, ReservedEntry { - name: "Forbidden", + name: "Disconnect", id: 8, }, ReservedEntry { - name: "NotFound", + name: "Redirect", id: 9, }, ReservedEntry { - name: "TooManyRequests", + name: "Shutdown", id: 10, }, ReservedEntry { - name: "InternalServerError", + name: "Error", id: 11, }, ReservedEntry { - name: "BadGateway", + name: "ErrorParsing", id: 12, }, ReservedEntry { - name: "ServiceUnavailable", + name: "ErrorBadVersion", id: 13, }, ReservedEntry { - name: "GatewayTimeout", + name: "BadRequest", id: 14, }, ReservedEntry { - name: "Identification", + name: "Unauthorized", id: 15, }, ReservedEntry { - name: "IdentificationResponse", + name: "Forbidden", id: 16, }, ReservedEntry { - name: "Register", + name: "NotFound", id: 17, }, ReservedEntry { - name: "RegisterResponse", + name: "TooManyRequests", id: 18, }, ReservedEntry { - name: "Ping", + name: "InternalServerError", id: 19, }, ReservedEntry { - name: "Pong", + name: "BadGateway", id: 20, }, + ReservedEntry { + name: "ServiceUnavailable", + id: 21, + }, + ReservedEntry { + name: "GatewayTimeout", + id: 22, + }, ]; const RESERVED_DATA_TYPES: &[ReservedEntry] = &[ ReservedEntry { - name: "Error", + name: "Version", id: 0, }, ReservedEntry { - name: "ErrorParsing", + name: "Id", id: 1, }, ReservedEntry { - name: "ErrorMessage", + name: "ClientNonce", id: 2, }, ReservedEntry { - name: "Version", + name: "ServerNonce", id: 3, }, ReservedEntry { - name: "Description", + name: "PublicKeys", id: 4, }, - ReservedEntry { - name: "Timestamp", - id: 5, - }, - ReservedEntry { name: "Id", id: 6 }, - ReservedEntry { - name: "ClientNonce", - id: 7, - }, - ReservedEntry { - name: "ServerNonce", - id: 8, - }, - ReservedEntry { - name: "PublicKeys", - id: 9, - }, ReservedEntry { name: "Signature", - id: 10, - }, - ReservedEntry { - name: "Connected", - id: 11, + id: 5, }, ReservedEntry { name: "PqSignature", + id: 6, + }, + ReservedEntry { + name: "Description", + id: 7, + }, + ReservedEntry { + name: "Connected", + id: 8, + }, + ReservedEntry { + name: "Timestamp", + id: 9, + }, + ReservedEntry { + name: "Error", + id: 10, + }, + ReservedEntry { + name: "ErrorParsing", + id: 11, + }, + ReservedEntry { + name: "ErrorMessage", id: 12, }, ]; diff --git a/type-map/src/lib.rs b/type-map/src/lib.rs index 4b44ed1..854fe38 100644 --- a/type-map/src/lib.rs +++ b/type-map/src/lib.rs @@ -1,6 +1,8 @@ -// try_new returns Result deliberately: the only failure mode is "id -// is in the reserved range", which carries no extra information worth an error -// type. The unit error is the intended API. +/* + * try_new returns Result deliberately: the only failure mode is "id + * is in the reserved range", which carries no extra information worth an error + * type. The unit error is the intended API. + */ #![allow(clippy::result_unit_err)] pub const INTERNAL_COMM_RESERVED: std::ops::Range = 0..32; diff --git a/wasm/src/client.rs b/wasm/src/client.rs index af3c9a5..f9ea26f 100644 --- a/wasm/src/client.rs +++ b/wasm/src/client.rs @@ -36,23 +36,54 @@ fn unexpected_response_type_error( )) } -/// Verify the host's authentication signature over the handshake response, -/// mirroring the native client (`client/src/lib.rs`). The signed payload is -/// `0x01 || id.to_be_bytes() || client_nonce.to_be_bytes() || host_nonce.to_be_bytes()`, -/// where `id` is the client id for login and the host-assigned id for register. -/// The Ed25519 signature is mandatory; the ML-DSA signature is verified only -/// when the host included one. -fn verify_host_signature( +/* + * Verify the host's signature over the challenge it issued (step 2), mirroring + * the native client (`client/src/lib.rs`). `id` is the client id for a login or + * `0` for a registration. The Ed25519 signature is mandatory; the ML-DSA + * signature is verified only when the host included one. + */ +fn verify_host_challenge( + challenge: &CommunicationValue, + tm: &mtp_codec::TypeMap, + host_pk: &mtp_crypto::PublicKeyBundle, + id: u64, + server_challenge: u128, +) -> Result<(), JsValue> { + let sig = match challenge.get_data(DataType::Signature.to_id(tm)) { + DataValue::Bytes(b) => b.clone(), + _ => return Err(js_error("missing host challenge signature")), + }; + let pq_sig = match challenge.get_data(DataType::PqSignature.to_id(tm)) { + DataValue::Bytes(b) => b.clone(), + _ => vec![], + }; + + let payload = mtp_crypto::auth::challenge_payload(id, server_challenge); + mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig) + .map_err(|_| js_error("host challenge signature invalid"))?; + if !pq_sig.is_empty() { + mtp_crypto::verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig) + .map_err(|_| js_error("host challenge PQ signature invalid"))?; + } + Ok(()) +} + +/* + * Verify the host's final confirmation (step 4): the echoed `client_nonce` and + * the host signature over the handshake transcript. `id` is the client id for a + * login and the host-assigned id for a register. + */ +fn verify_host_final( resp: &CommunicationValue, tm: &mtp_codec::TypeMap, host_pk: &mtp_crypto::PublicKeyBundle, id: u64, client_nonce: u128, + server_challenge: u128, ) -> Result<(), JsValue> { - let host_new_nonce = match resp.get_data(DataType::Timestamp.to_id(tm)) { - DataValue::UnsignedNumber(n) => *n, - _ => return Err(js_error("missing host nonce")), - }; + if *resp.get_data(DataType::ClientNonce.to_id(tm)) != DataValue::UnsignedNumber(client_nonce) { + return Err(js_error("nonce mismatch")); + } let host_sig = match resp.get_data(DataType::Signature.to_id(tm)) { DataValue::Bytes(b) => b.clone(), _ => return Err(js_error("missing host signature")), @@ -62,12 +93,7 @@ fn verify_host_signature( _ => vec![], }; - let mut payload = Vec::new(); - payload.push(0x01); - payload.extend_from_slice(&id.to_be_bytes()); - payload.extend_from_slice(&client_nonce.to_be_bytes()); - payload.extend_from_slice(&host_new_nonce.to_be_bytes()); - + let payload = mtp_crypto::auth::host_final_payload(id, client_nonce, server_challenge); mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &host_sig) .map_err(|_| js_error("host signature invalid"))?; if !host_pq_sig.is_empty() { @@ -219,26 +245,68 @@ impl WasmClient { let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes) .map_err(|e| js_error(&format!("invalid keyring: {}", e)))?; + let tm = mtp_codec::TypeMap::latest(); let version_str = format!("{}", PROTOCOL_VERSION); + + let transport = + WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?; + let inner = transport.inner().clone(); + + // 1. Send the unsigned Identification hello. + let 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)) + .to_bytes() + .map_err(|e| js_error(&format!("encode failed: {}", e)))?; + transport.send_frame(&hello).await?; + + // 2. Receive and verify the host's challenge. + let challenge_bytes = transport.read_one_frame().await?; + let challenge = CommunicationValue::from_bytes(&challenge_bytes) + .map_err(|e| js_error(&format!("parse challenge: {}", e)))?; + let expected = CommunicationType::Challenge.to_id(&tm); + if challenge.get_type() != expected { + self.set_state(ConnectionState::Disconnected); + return Err(unexpected_response_type_error( + "auth_connect challenge", + expected, + challenge.get_type(), + &challenge_bytes, + &challenge, + )); + } + let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { + DataValue::UnsignedNumber(n) => *n, + _ => { + self.set_state(ConnectionState::Disconnected); + return Err(js_error("missing server challenge")); + } + }; + if let Err(e) = + verify_host_challenge(&challenge, &tm, &host_pk, client_id, server_challenge) + { + self.set_state(ConnectionState::Disconnected); + return Err(e); + } + + // 3. Sign the host's challenge and send the proof. let mut nonce_bytes = [0u8; 16]; getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; let client_nonce = u128::from_be_bytes(nonce_bytes); - // Build signature payload: version || client_id || client_nonce - let mut sig_payload = Vec::new(); - sig_payload.extend_from_slice(version_str.as_bytes()); - sig_payload.extend_from_slice(&client_id.to_be_bytes()); - sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - + let proof_payload = mtp_crypto::auth::login_proof_payload( + &version_str, + client_id, + server_challenge, + client_nonce, + ); let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; let signature = signer - .sign(&sig_payload) + .sign(&proof_payload) .map_err(|e| js_error(&format!("signature failed: {}", e)))?; - let frame = CommunicationValue::new(CommunicationType::Identification) - .add_typed_default(DataType::Version, DataValue::Str(version_str)) - .add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128)) + let proof = CommunicationValue::new(CommunicationType::ChallengeResponse) .add_typed_default( DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce), @@ -246,18 +314,12 @@ impl WasmClient { .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) .to_bytes() .map_err(|e| js_error(&format!("encode failed: {}", e)))?; + transport.send_frame(&proof).await?; - let transport = - WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?; - let inner = transport.inner().clone(); - transport.send_frame(&frame).await?; - - // Read and verify the host's IdentificationResponse + // 4. Receive and verify the host's final confirmation. let response = transport.read_one_frame().await?; let resp_comm = CommunicationValue::from_bytes(&response) .map_err(|e| js_error(&format!("parse response: {}", e)))?; - - let tm = mtp_codec::TypeMap::latest(); let resp_type = resp_comm.get_type(); let expected_type = CommunicationType::IdentificationResponse.to_id(&tm); if resp_type != expected_type { @@ -276,15 +338,15 @@ impl WasmClient { return Err(js_error("host rejected authentication")); } - // Verify echoed nonce - let echo_nonce = resp_comm.get_data(DataType::ClientNonce.to_id(&tm)); - if *echo_nonce != DataValue::UnsignedNumber(client_nonce) { - self.set_state(ConnectionState::Disconnected); - return Err(js_error("nonce mismatch")); - } - - // Verify the host's signature over the handshake (login: id is client_id). - if let Err(e) = verify_host_signature(&resp_comm, &tm, &host_pk, client_id, client_nonce) { + // Verify echoed nonce + host signature (login: id is client_id). + if let Err(e) = verify_host_final( + &resp_comm, + &tm, + &host_pk, + client_id, + client_nonce, + server_challenge, + ) { self.set_state(ConnectionState::Disconnected); return Err(e); } @@ -335,46 +397,80 @@ impl WasmClient { let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes) .map_err(|e| js_error(&format!("invalid keyring: {}", e)))?; + let tm = mtp_codec::TypeMap::latest(); let version_str = format!("{}", PROTOCOL_VERSION); - let mut nonce_bytes = [0u8; 16]; - getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; - let client_nonce = u128::from_be_bytes(nonce_bytes); - let pk_bytes = keyring.public_key_bundle().as_bytes(); - // Build signature payload: version || client_nonce || pk_bytes - let mut sig_payload = Vec::new(); - sig_payload.extend_from_slice(version_str.as_bytes()); - sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); - sig_payload.extend_from_slice(&pk_bytes); - - let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) - .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; - let signature = signer - .sign(&sig_payload) - .map_err(|e| js_error(&format!("signature failed: {}", e)))?; - - let frame = CommunicationValue::new(CommunicationType::Register) - .add_typed_default(DataType::Version, DataValue::Str(version_str)) - .add_typed_default( - DataType::ClientNonce, - DataValue::UnsignedNumber(client_nonce), - ) - .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes)) - .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) - .to_bytes() - .map_err(|e| js_error(&format!("encode failed: {}", e)))?; - let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?; let inner = transport.inner().clone(); - transport.send_frame(&frame).await?; + // 1. Send the unsigned Register hello (version + public-key bundle). + let 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())) + .to_bytes() + .map_err(|e| js_error(&format!("encode failed: {}", e)))?; + transport.send_frame(&hello).await?; + + // 2. Receive and verify the host's challenge (register binds id = 0). + let challenge_bytes = transport.read_one_frame().await?; + let challenge = CommunicationValue::from_bytes(&challenge_bytes) + .map_err(|e| js_error(&format!("parse challenge: {}", e)))?; + let expected = CommunicationType::Challenge.to_id(&tm); + if challenge.get_type() != expected { + self.set_state(ConnectionState::Disconnected); + return Err(unexpected_response_type_error( + "auth_register challenge", + expected, + challenge.get_type(), + &challenge_bytes, + &challenge, + )); + } + let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) { + DataValue::UnsignedNumber(n) => *n, + _ => { + self.set_state(ConnectionState::Disconnected); + return Err(js_error("missing server challenge")); + } + }; + if let Err(e) = verify_host_challenge(&challenge, &tm, &host_pk, 0, server_challenge) { + self.set_state(ConnectionState::Disconnected); + return Err(e); + } + + // 3. Sign the host's challenge over the bundle and send the proof. + let mut nonce_bytes = [0u8; 16]; + getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; + let client_nonce = u128::from_be_bytes(nonce_bytes); + + let proof_payload = mtp_crypto::auth::register_proof_payload( + &version_str, + &pk_bytes, + server_challenge, + client_nonce, + ); + let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) + .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; + let signature = signer + .sign(&proof_payload) + .map_err(|e| js_error(&format!("signature failed: {}", e)))?; + + let proof = CommunicationValue::new(CommunicationType::ChallengeResponse) + .add_typed_default( + DataType::ClientNonce, + DataValue::UnsignedNumber(client_nonce), + ) + .add_typed_default(DataType::Signature, DataValue::Bytes(signature)) + .to_bytes() + .map_err(|e| js_error(&format!("encode failed: {}", e)))?; + transport.send_frame(&proof).await?; + + // 4. Receive the host's final confirmation; extract + verify assigned id. let response = transport.read_one_frame().await?; let resp_comm = CommunicationValue::from_bytes(&response) .map_err(|e| js_error(&format!("parse response: {}", e)))?; - - let tm = mtp_codec::TypeMap::latest(); let resp_type = resp_comm.get_type(); let expected_type = CommunicationType::RegisterResponse.to_id(&tm); if resp_type != expected_type { @@ -393,12 +489,6 @@ impl WasmClient { return Err(js_error("host rejected registration")); } - let echo = resp_comm.get_data(DataType::ClientNonce.to_id(&tm)); - if *echo != DataValue::UnsignedNumber(client_nonce) { - self.set_state(ConnectionState::Disconnected); - return Err(js_error("nonce mismatch")); - } - let assigned_id = match resp_comm.get_data(DataType::Id.to_id(&tm)) { DataValue::UnsignedNumber(n) => *n as u64, _ => { @@ -407,10 +497,15 @@ impl WasmClient { } }; - // Verify the host's signature over the handshake (register: id is the - // host-assigned id). - if let Err(e) = verify_host_signature(&resp_comm, &tm, &host_pk, assigned_id, client_nonce) - { + // Verify echoed nonce + host signature (register: id is host-assigned). + if let Err(e) = verify_host_final( + &resp_comm, + &tm, + &host_pk, + assigned_id, + client_nonce, + server_challenge, + ) { self.set_state(ConnectionState::Disconnected); return Err(e); } diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 183730d..7727c88 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -4,6 +4,7 @@ pub mod error; pub mod message; pub mod transport; +#[cfg(not(test))] use wasm_bindgen::prelude::*; #[cfg(not(test))] diff --git a/wasm/src/message.rs b/wasm/src/message.rs index 9ee9452..bad844f 100644 --- a/wasm/src/message.rs +++ b/wasm/src/message.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue}; use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm}; -use mtp_type_map::communication_type_name; +use mtp_type_map::{communication_type_name, TypeMap}; use crate::error::js_error; @@ -52,10 +52,10 @@ pub fn build_demo_message( let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key) .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; - // Encrypted container (DataTypeId 1 = arbitrary custom) + // Encrypted container let inner_enc = DataValue::Container(vec![ - (DataTypeId(1), DataValue::Str("secret inner data".into())), - (DataTypeId(2), DataValue::UnsignedNumber(42)), + (DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("secret inner data".into())), + (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(42)), ]); let mut dv_enc = inner_enc; dv_enc @@ -64,8 +64,8 @@ pub fn build_demo_message( // Signed container let inner_sig = DataValue::Container(vec![ - (DataTypeId(1), DataValue::Str("signed by client".into())), - (DataTypeId(2), DataValue::UnsignedNumber(99)), + (DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("signed by client".into())), + (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(99)), ]); let mut dv_sig = inner_sig; dv_sig @@ -75,10 +75,10 @@ pub fn build_demo_message( // Signed + encrypted container let inner_sec = DataValue::Container(vec![ ( - DataTypeId(1), + DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("signed+encrypted payload".into()), ), - (DataTypeId(2), DataValue::UnsignedNumber(7)), + (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(7)), ]); let mut dv_sec = inner_sec; dv_sec @@ -115,24 +115,24 @@ pub fn parse_auth_response(response: &[u8]) -> Result { let comm = CommunicationValue::from_bytes(response) .map_err(|e| js_error(&format!("parse failed: {}", e)))?; - let connected = matches!(comm.get_data(DataTypeId(11)), DataValue::BoolTrue); + let connected = matches!(comm.get_data(DataType::Connected.to_id(&TypeMap::latest())), DataValue::BoolTrue); - let client_nonce = match comm.get_data(DataTypeId(7)) { + let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) { DataValue::UnsignedNumber(n) => Some(*n), _ => None, }; - let assigned_id = match comm.get_data(DataTypeId(6)) { + let assigned_id = match comm.get_data(DataType::Id.to_id(&TypeMap::latest())) { DataValue::UnsignedNumber(n) => Some(*n as u64), _ => None, }; - let timestamp = match comm.get_data(DataTypeId(5)) { + let timestamp = match comm.get_data(DataType::Timestamp.to_id(&TypeMap::latest())) { DataValue::UnsignedNumber(n) => Some(*n), _ => None, }; - let signature = match comm.get_data(DataTypeId(10)) { + let signature = match comm.get_data(DataType::Signature.to_id(&TypeMap::latest())) { DataValue::Bytes(b) => Some(b.clone()), _ => None, }; @@ -252,15 +252,16 @@ mod tests { fn build_ping_frame_roundtrip() { let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed"); let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + let tm = TypeMap::latest(); - assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping + assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm)); assert_eq!(cv.get_sender(), 42); assert_eq!( - cv.get_data(DataTypeId(4)), + cv.get_data(DataType::Description.to_id(&tm)), &DataValue::Str("test-ping".into()) ); assert_eq!( - cv.get_data(DataTypeId(5)), + cv.get_data(DataType::Timestamp.to_id(&tm)), &DataValue::UnsignedNumber(1234567890) ); } @@ -270,16 +271,17 @@ mod tests { let payload = b"attachment-data"; let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed"); let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + let tm = TypeMap::latest(); - assert_eq!(cv.get_type(), CommunicationTypeId(19)); + assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm)); assert_eq!(cv.get_sender(), 99); assert_eq!( - cv.get_data(DataTypeId(4)), + cv.get_data(DataType::Description.to_id(&tm)), &DataValue::Str("with-data".into()) ); - assert_eq!(cv.get_data(DataTypeId(5)), &DataValue::UnsignedNumber(555)); + assert_eq!(cv.get_data(DataType::Timestamp.to_id(&tm)), &DataValue::UnsignedNumber(555)); assert_eq!( - cv.get_data(DataTypeId(6)), + cv.get_data(DataType::Id.to_id(&tm)), &DataValue::Bytes(payload.to_vec()) ); } @@ -304,11 +306,12 @@ mod tests { let bytes = result.unwrap(); let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + let tm = TypeMap::latest(); - assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping + assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm)); assert_eq!(cv.get_sender(), 7); assert_eq!( - cv.get_data(DataTypeId(4)), + cv.get_data(DataType::Description.to_id(&tm)), &DataValue::Str("MTP WASM Demo".into()) ); } diff --git a/wasm/src/transport.rs b/wasm/src/transport.rs index 1648d58..06713b7 100644 --- a/wasm/src/transport.rs +++ b/wasm/src/transport.rs @@ -223,7 +223,10 @@ impl WasmTransport { let incoming = self.inner.incoming_unidirectional_streams(); let reader_fn = match js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader")) { - Ok(f) => f.dyn_into::().unwrap(), + Ok(f) => match f.dyn_into::() { + Ok(f) => f, + Err(_) => return, + }, Err(_) => return, }; let reader_val = match reader_fn.call0(&incoming) { @@ -233,7 +236,10 @@ impl WasmTransport { loop { let read_fn = match js_sys::Reflect::get(&reader_val, &JsValue::from_str("read")) { - Ok(f) => f.dyn_into::().unwrap(), + Ok(f) => match f.dyn_into::() { + Ok(f) => f, + Err(_) => break, + }, Err(_) => break, }; let result = match read_fn.call0(&reader_val) {