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).
This commit is contained in:
Alex Emmet 2026-06-26 17:08:48 +02:00
commit f4118f28ba
25 changed files with 1032 additions and 667 deletions

View file

@ -68,14 +68,24 @@ The host's `accept()` method:
6. Returns `None` if the version is unsupported 6. Returns `None` if the version is unsupported
7. Returns an `MTPConnection` with the negotiated version otherwise 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 - **Login** (`CommunicationType::Identification`, ID 15): version, client ID
- **Register** (`CommunicationType::Register`, ID 17): public keys, nonce, signature - **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. } | | CommValue{ Ident. } |
| Version -> "2.0" | | Version -> "2.0" |
| Id -> 8765 | | Id -> 8765 |
| Nonce -> ... | | (unsigned hello; auth |
| Signature -> ... | | challenge follows) |
|----------------------->| |----------------------->|
| | registry.negotiate(&[Version(2,0)]) | | registry.negotiate(&[Version(2,0)])
| | -> Some(Version(2,0)) | | -> Some(Version(2,0))

3
Cargo.lock generated
View file

@ -976,6 +976,9 @@ dependencies = [
"mtp-host", "mtp-host",
"mtp-transport", "mtp-transport",
"mtp-type-map", "mtp-type-map",
"rand 0.8.6",
"rcgen",
"tokio",
] ]
[[package]] [[package]]

View file

@ -79,3 +79,8 @@ host = ["dep:mtp-host", "mtp-codec/registry", "mtp-transport/host"]
# MTP client - outgoing QUIC connections to a host. # MTP client - outgoing QUIC connections to a host.
client = ["dep:mtp-client"] client = ["dep:mtp-client"]
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
rcgen = "0.14"
rand = "0.8"

View file

@ -109,15 +109,22 @@ let config = ClientConfig {
let conn = MTPClient::auth_connect(config, &keys, &host_pk).await?; let conn = MTPClient::auth_connect(config, &keys, &host_pk).await?;
``` ```
Protocol: Protocol (challenge-response, the host issues the freshness):
1. Client generates a random nonce 1. Client sends an unsigned `Identification` hello (version, client ID)
2. Builds a signature payload: `version || client_id || client_nonce` 2. Host replies with a `Challenge` carrying a fresh random `server_challenge`
3. Signs with Ed25519 (and optionally ML-DSA-65) and the host's signature over it; the client verifies that signature
4. Sends `Identification` frame containing version, client ID, nonce, signature(s) 3. Client generates a random `client_nonce` and signs
5. Host responds with `IdentificationResponse` containing echoed nonce, host `version || client_id || server_challenge || client_nonce` with Ed25519
nonce, and host signature (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 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 ### Registration
```rust ```rust
@ -134,13 +141,16 @@ let id = conn.client_id;
let keyring_bytes = keyring.to_bytes(); let keyring_bytes = keyring.to_bytes();
``` ```
Protocol: Protocol (challenge-response):
1. Client generates a random nonce 1. Client sends an unsigned `Register` hello (version, public key bundle)
2. Builds a signature payload: `version || client_nonce || public_key_bytes` 2. Host replies with a `Challenge` carrying a fresh random `server_challenge`
3. Signs with Ed25519 (and optionally ML-DSA-65) (signed by the host); the client verifies that signature
4. Sends `Register` frame containing version, nonce, public key bundle, signature(s) 3. Client generates a random `client_nonce` and signs
5. Host assigns a new client ID, responds with `RegisterResponse` containing `version || server_challenge || client_nonce || public_key_bytes` with
the ID, echoed nonce, host nonce, and host signature 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 6. Client verifies the host signature and nonce echo
## Key Material ## 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 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): key from the keyring):
```rust ```rust

View file

@ -129,12 +129,19 @@ let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
// -> Some(Version(2, 0)) if both versions are registered // -> Some(Version(2, 0)) if both versions are registered
``` ```
## Authentication Flow (crypto feature) ## Authentication Flow
When `require_authentication` is `true`, `accept()` runs an authenticated When `require_authentication` is `true`, `accept()` runs a mutually-authenticated
handshake before returning the connection. The flow is: **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 Client Host
@ -142,26 +149,35 @@ Client Host
| QUIC connect | | QUIC connect |
|---------------------------------------->| |---------------------------------------->|
| | | |
| Identification { | | Identification { Version, Id } | (unsigned hello)
| Version, Id, ClientNonce, | |---------------------------------------->|
| Signature, [PqSignature] | | | 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 proof over server_challenge
| | verify Ed25519 (and optional ML-DSA) sig
| IdentificationResponse { | | IdentificationResponse { |
| Connected=true, ClientNonce(echoed), | | Connected=true, Id, |
| Id, Timestamp(new_nonce), | | ClientNonce(echoed), |
| Signature, [PqSignature] | | 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 Client Host
@ -170,23 +186,32 @@ Client Host
|---------------------------------------->| |---------------------------------------->|
| | | |
| Register { | | Register { |
| Version, ClientNonce, | | Version, | (unsigned hello)
| PublicKeys (serialized PublicKeyBundle), | PublicKeys (serialized PublicKeyBundle)
| Signature, [PqSignature] |
| } | | } |
|---------------------------------------->| |---------------------------------------->|
| | extract PublicKeyBundle from frame | | generate random server_challenge
| | verify Ed25519 (and optional ML-DSA) sig | 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 | | call complete_register(bundle) -> new_id
| RegisterResponse { | | RegisterResponse { |
| Connected=true, ClientNonce(echoed), | | Connected=true, Id(new_id), |
| Id, Timestamp(new_nonce), | | ClientNonce(echoed), |
| Signature, [PqSignature] | | 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 After a successful handshake, `accept()` returns an `MTPConnection` with
`auth_state = Authenticated`, `client_id` set, and `client_public_key` `auth_state = Authenticated`, `client_id` set, and `client_public_key`

View file

@ -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. 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 ## Getting Started

View file

@ -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. 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 ```typescript
const confirmedId = await client.auth_connect( 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 Exchange (challenge-response): the client sends an unsigned `Identification`
responds with a signed `IdentificationResponse`. Returns the confirmed client ID. 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 ```typescript
const newId = await client.auth_register( 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 Exchange (challenge-response): the client sends an unsigned `Register` hello with
assigns a new ID and responds with a signed `RegisterResponse`. Returns the 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. newly assigned client ID.
## Sending and Receiving Messages ## Sending and Receiving Messages

View file

@ -22,7 +22,7 @@ pub struct ClientConfig {
pub client_id: u64, pub client_id: u64,
} }
// Established MTP connection with a single negotiated version. /* Established MTP connection with a single negotiated version. */
pub struct MTPConnection { pub struct MTPConnection {
pub version: Version, pub version: Version,
pub sender: Sender, pub sender: Sender,
@ -78,6 +78,113 @@ impl MTPClient {
} }
/* ===== Authentication ===== */ /* ===== 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")] #[cfg(feature = "crypto")]
impl MTPClient { impl MTPClient {
pub async fn auth_connect( pub async fn auth_connect(
@ -85,59 +192,82 @@ impl MTPClient {
keys: &mtp_crypto::Keyring, keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle, host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> { ) -> Result<MTPConnection, CommunicationError> {
use mtp_crypto::{ use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth};
Ed25519Signer, MlDsaSigner, SignatureScheme, verify_ed25519, verify_ml_dsa,
};
let (sender, receiver) = let (sender, receiver) =
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
// 1. Build and send Identification message immediately (no greeting) let tm = mtp_codec::TypeMap::latest();
let client_nonce: u128 = rand::random();
let version_str = format!("{}", PROTOCOL_VERSION); let version_str = format!("{}", PROTOCOL_VERSION);
let mut sig_payload = Vec::new(); // 1. Send the unsigned Identification hello (version + claimed id).
sig_payload.extend_from_slice(version_str.as_bytes()); let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
sig_payload.extend_from_slice(&config.client_id.to_be_bytes()); .add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
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))
.add_typed_default( .add_typed_default(
DataType::Id, DataType::Id,
DataValue::UnsignedNumber(config.client_id as u128), 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( .add_typed_default(
DataType::ClientNonce, DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce), DataValue::UnsignedNumber(client_nonce),
) )
.add_typed_default(DataType::Signature, DataValue::Bytes(signature)); .add_typed_default(DataType::Signature, DataValue::Bytes(signature));
if !keys.sig_pq_secret_key.as_bytes().is_empty() { 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) let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?; .map_err(|e| CommunicationError::Other(e.to_string()))?;
let pq_signature = pq_signer let pq_signature = pq_signer
.sign(&sig_payload) .sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?; .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?; // 4. Receive and verify the host's final confirmation.
// 2. Receive host response (single message)
let response = receiver.receive().await?; let response = receiver.receive().await?;
let tm = mtp_codec::TypeMap::latest();
let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm); let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm);
if response.get_type() != expected_type { if response.get_type() != expected_type {
return Err(unexpected_response_type_error( return Err(unexpected_response_type_error(
@ -146,81 +276,15 @@ impl MTPClient {
&response, &response,
)); ));
} }
check_connected(&response, &tm, "Server rejected authentication")?;
let connected = response.get_data(DataType::Connected.to_id(&tm)); verify_host_final(
match connected { &response,
DataValue::BoolTrue => {} &tm,
DataValue::BoolFalse => { host_public_key_bundle,
return Err(CommunicationError::AuthenticationFailed( config.client_id,
"Server rejected authentication".into(), client_nonce,
)); server_challenge,
} )?;
_ => {
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 ===== */
Ok(MTPConnection { Ok(MTPConnection {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
@ -236,59 +300,72 @@ impl MTPClient {
keys: &mtp_crypto::Keyring, keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle, host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> { ) -> Result<MTPConnection, CommunicationError> {
use mtp_crypto::{ use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth};
Ed25519Signer, MlDsaSigner, SignatureScheme, verify_ed25519, verify_ml_dsa,
};
let (sender, receiver) = let (sender, receiver) =
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?; mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
// 1. Build and send Register message immediately let tm = mtp_codec::TypeMap::latest();
let client_nonce: u128 = rand::random();
let version_str = format!("{}", PROTOCOL_VERSION); let version_str = format!("{}", PROTOCOL_VERSION);
let pk_bundle = keys.public_key_bundle(); let pk_bundle = keys.public_key_bundle();
let pk_bytes = pk_bundle.as_bytes(); let pk_bytes = pk_bundle.as_bytes();
let mut sig_payload = Vec::new(); // 1. Send the unsigned Register hello (version + public-key bundle).
sig_payload.extend_from_slice(version_str.as_bytes()); let register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); .add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
sig_payload.extend_from_slice(&pk_bytes); .add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
sender.send(&register).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) let signer = Ed25519Signer::new(&keys.sig_cl_secret_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?; .map_err(|e| CommunicationError::Other(e.to_string()))?;
/* ===== Signature ===== */
let signature = signer let signature = signer
.sign(&sig_payload) .sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?; .map_err(|e| CommunicationError::Other(e.to_string()))?;
let mut register = CommunicationValue::new(mtp_codec::CommunicationType::Register) let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse)
.add_typed_default(DataType::Version, DataValue::Str(version_str))
.add_typed_default( .add_typed_default(
DataType::ClientNonce, DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce), DataValue::UnsignedNumber(client_nonce),
) )
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes))
.add_typed_default(DataType::Signature, DataValue::Bytes(signature)); .add_typed_default(DataType::Signature, DataValue::Bytes(signature));
if !keys.sig_pq_secret_key.as_bytes().is_empty() { 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) let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?; .map_err(|e| CommunicationError::Other(e.to_string()))?;
let pq_signature = pq_signer let pq_signature = pq_signer
.sign(&sig_payload) .sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?; .map_err(|e| CommunicationError::Other(e.to_string()))?;
register = proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
register.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
} }
/* ===== End Signature ===== */ sender.send(&proof).await?;
sender.send(&register).await?; // 4. Receive the host's final confirmation; extract the assigned id and
// verify the host signature binds to it.
// 2. Receive host response (single message)
let response = receiver.receive().await?; let response = receiver.receive().await?;
let tm = mtp_codec::TypeMap::latest();
let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm); let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm);
if response.get_type() != expected_type { if response.get_type() != expected_type {
return Err(unexpected_response_type_error( return Err(unexpected_response_type_error(
@ -297,97 +374,30 @@ impl MTPClient {
&response, &response,
)); ));
} }
check_connected(&response, &tm, "Server rejected registration")?;
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(),
));
}
}
let assigned_id = match response.get_data(DataType::Id.to_id(&tm)) { 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( return Err(CommunicationError::AuthenticationFailed(
"Missing assigned ID".into(), "Missing assigned ID".into(),
)); ));
} }
}; };
verify_host_final(
let echo_nonce = response.get_data(DataType::ClientNonce.to_id(&tm)); &response,
match echo_nonce { &tm,
DataValue::UnsignedNumber(n) if *n == client_nonce as u128 => {} host_public_key_bundle,
_ => { assigned_id,
return Err(CommunicationError::AuthenticationFailed( client_nonce,
"Nonce mismatch".into(), server_challenge,
)); )?;
}
}
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 ===== */
Ok(MTPConnection { Ok(MTPConnection {
version: PROTOCOL_VERSION, version: PROTOCOL_VERSION,
sender, sender,
receiver, receiver,
auth_state: AuthState::Authenticated, auth_state: AuthState::Authenticated,
client_id: assigned_id as u64, client_id: assigned_id,
}) })
} }
} }

View file

@ -345,7 +345,7 @@ impl CommunicationValue {
let data = if is_encrypted { let data = if is_encrypted {
let mut map = BTreeMap::new(); let mut map = BTreeMap::new();
map.insert( map.insert(
DataTypeId(0), DataType::Version.to_id(&TypeMap::latest()),
DataValue::EncryptedContainer(data_bytes.to_vec()), DataValue::EncryptedContainer(data_bytes.to_vec()),
); );
map map
@ -582,7 +582,7 @@ mod tests {
assert_eq!(total_len as usize + 4, bytes.len()); assert_eq!(total_len as usize + 4, bytes.len());
let typ = c.read_u16::<BigEndian>().expect("read type"); let typ = c.read_u16::<BigEndian>().expect("read type");
assert_eq!(typ, 1); assert_eq!(typ, 12);
let flags = c.read_u8().expect("read flags"); let flags = c.read_u8().expect("read flags");
assert_eq!(flags & 0b0000_0111, 0); assert_eq!(flags & 0b0000_0111, 0);
@ -602,7 +602,7 @@ mod tests {
assert_eq!(total_len as usize + 4, bytes.len()); assert_eq!(total_len as usize + 4, bytes.len());
let typ = c.read_u16::<BigEndian>().expect("read type"); let typ = c.read_u16::<BigEndian>().expect("read type");
assert_eq!(typ, 2); assert_eq!(typ, 13);
let flags = c.read_u8().expect("read flags"); let flags = c.read_u8().expect("read flags");
assert_eq!(flags & 0b0000_0111, 0b0000_0111); assert_eq!(flags & 0b0000_0111, 0b0000_0111);
@ -621,15 +621,16 @@ mod tests {
#[test] #[test]
fn test_roundtrip_complex() { fn test_roundtrip_complex() {
let tm = TypeMap::latest();
let cv = CommunicationValue::new(CommunicationType::Disconnect) let cv = CommunicationValue::new(CommunicationType::Disconnect)
.with_id(1234) .with_id(1234)
.with_sender(111) .with_sender(111)
.with_receiver(222) .with_receiver(222)
.add_data(DataTypeId(1), DataValue::Str("alice".to_string())) .add_typed_default(DataType::Id, DataValue::Str("alice".to_string()))
.add_data(DataTypeId(2), DataValue::SignedNumber(42)) .add_typed_default(DataType::ClientNonce, DataValue::SignedNumber(42))
.add_data(DataTypeId(3), DataValue::BoolTrue) .add_typed_default(DataType::ServerNonce, DataValue::BoolTrue)
.add_data( .add_typed_default(
DataTypeId(4), DataType::PublicKeys,
DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]), 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_id(), 1234);
assert_eq!(decoded.get_sender(), 111); assert_eq!(decoded.get_sender(), 111);
assert_eq!(decoded.get_receiver(), 222); 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!( assert_eq!(
decoded.get_data(DataTypeId(1)), decoded.get_data(DataType::Id.to_id(&tm)),
&DataValue::Str("alice".to_string()) &DataValue::Str("alice".to_string())
); );
assert_eq!( assert_eq!(
decoded.get_data(DataTypeId(2)), decoded.get_data(DataType::ClientNonce.to_id(&tm)),
&DataValue::SignedNumber(42) &DataValue::SignedNumber(42)
); );
} }
@ -668,7 +669,7 @@ mod tests {
.with_id(7) .with_id(7)
.with_sender(1) .with_sender(1)
.with_receiver(2) .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()); assert!(cv.sign_frame(SigAlgorithm::ED25519, &signer).is_some());
@ -691,7 +692,7 @@ mod tests {
let (_, other_sk, _) = Ed25519Signer::generate(); let (_, other_sk, _) = Ed25519Signer::generate();
let mut cv = CommunicationValue::new(CommunicationType::Ping) 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()); assert!(cv.sign_frame(SigAlgorithm::ED25519, &signer).is_some());
let wrong = Ed25519Signer::new(&other_sk).unwrap(); let wrong = Ed25519Signer::new(&other_sk).unwrap();

View file

@ -9,6 +9,9 @@ use std::io::Cursor;
use mtp_common::CodecError; use mtp_common::CodecError;
use mtp_type_map::DataTypeId; use mtp_type_map::DataTypeId;
#[cfg(test)]
use mtp_type_map::{DataType, TypeMap};
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
use mtp_crypto::{EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm, SignatureScheme}; use mtp_crypto::{EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm, SignatureScheme};
@ -151,11 +154,13 @@ impl DataValue {
const KIND_NULL: u8 = 0xFF; 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 * Smallest possible encoded entry, used to cap pre-reservation when
/// allocation from an attacker-controlled count. A bool/null entry in a * decoding containers/arrays so a small frame cannot force a huge
/// container is 3 bytes (1 kind + 2 key); a bare value in an array is 1 * allocation from an attacker-controlled count. A bool/null entry in a
/// byte, so 1 is the safe lower bound shared by both. * 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; const MIN_ENTRY_BYTES: usize = 1;
pub fn container_from_map(map: &BTreeMap<DataTypeId, DataValue>) -> DataValue { pub fn container_from_map(map: &BTreeMap<DataTypeId, DataValue>) -> DataValue {
@ -954,8 +959,6 @@ impl Hash for DataValue {
mod tests { mod tests {
use super::*; 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)>) { fn container_roundtrip(values: Vec<(DataTypeId, DataValue)>) {
let dv = DataValue::Container(values.clone()); let dv = DataValue::Container(values.clone());
let bytes = dv.to_bytes().expect("encode failed"); let bytes = dv.to_bytes().expect("encode failed");
@ -972,9 +975,10 @@ mod tests {
#[test] #[test]
fn test_bool_in_container() { fn test_bool_in_container() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::BoolTrue), (DataType::Id.to_id(&tm), DataValue::BoolTrue),
(DataTypeId(2), DataValue::BoolFalse), (DataType::ClientNonce.to_id(&tm), DataValue::BoolFalse),
]); ]);
} }
@ -995,54 +999,60 @@ mod tests {
#[test] #[test]
fn test_signed_number_in_container() { fn test_signed_number_in_container() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::SignedNumber(0)), (DataType::Version.to_id(&tm), DataValue::SignedNumber(0)),
(DataTypeId(2), DataValue::SignedNumber(42)), (DataType::Id.to_id(&tm), DataValue::SignedNumber(42)),
(DataTypeId(3), DataValue::SignedNumber(-42)), (DataType::ClientNonce.to_id(&tm), DataValue::SignedNumber(-42)),
(DataTypeId(4), DataValue::SignedNumber(i128::MAX)), (DataType::ServerNonce.to_id(&tm), DataValue::SignedNumber(i128::MAX)),
(DataTypeId(5), DataValue::SignedNumber(i128::MIN)), (DataType::PublicKeys.to_id(&tm), DataValue::SignedNumber(i128::MIN)),
]); ]);
} }
#[test] #[test]
fn test_unsigned_number_in_container() { fn test_unsigned_number_in_container() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::UnsignedNumber(0)), (DataType::Version.to_id(&tm), DataValue::UnsignedNumber(0)),
(DataTypeId(2), DataValue::UnsignedNumber(42)), (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)),
(DataTypeId(3), DataValue::UnsignedNumber(u128::MAX)), (DataType::ClientNonce.to_id(&tm), DataValue::UnsignedNumber(u128::MAX)),
]); ]);
} }
#[test] #[test]
fn test_float_in_container() { fn test_float_in_container() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::Float(0, 0)), (DataType::Version.to_id(&tm), DataValue::Float(0, 0)),
(DataTypeId(2), DataValue::Float(2, 12345)), (DataType::Id.to_id(&tm), DataValue::Float(2, 12345)),
(DataTypeId(3), DataValue::Float(255, 4294967295)), (DataType::ClientNonce.to_id(&tm), DataValue::Float(255, 4294967295)),
]); ]);
} }
#[test] #[test]
fn test_str_in_container() { fn test_str_in_container() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::Str(String::new())), (DataType::Version.to_id(&tm), DataValue::Str(String::new())),
(DataTypeId(2), DataValue::Str("hello".to_string())), (DataType::Id.to_id(&tm), DataValue::Str("hello".to_string())),
(DataTypeId(3), DataValue::Str("a".repeat(1000))), (DataType::ClientNonce.to_id(&tm), DataValue::Str("a".repeat(1000))),
]); ]);
} }
#[test] #[test]
fn test_bytes_in_container() { fn test_bytes_in_container() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::Bytes(vec![])), (DataType::Version.to_id(&tm), DataValue::Bytes(vec![])),
(DataTypeId(2), DataValue::Bytes(vec![0x00, 0xFF, 0xAB])), (DataType::Id.to_id(&tm), DataValue::Bytes(vec![0x00, 0xFF, 0xAB])),
(DataTypeId(3), DataValue::Bytes(vec![0x42; 100])), (DataType::ClientNonce.to_id(&tm), DataValue::Bytes(vec![0x42; 100])),
]); ]);
} }
#[test] #[test]
fn test_null_in_container() { 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] #[test]
@ -1070,24 +1080,26 @@ mod tests {
#[test] #[test]
fn test_container_mixed_roundtrip() { fn test_container_mixed_roundtrip() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
(DataTypeId(1), DataValue::BoolTrue), (DataType::Version.to_id(&tm), DataValue::BoolTrue),
(DataTypeId(2), DataValue::SignedNumber(-100)), (DataType::Id.to_id(&tm), DataValue::SignedNumber(-100)),
(DataTypeId(3), DataValue::Str("test".to_string())), (DataType::ClientNonce.to_id(&tm), DataValue::Str("test".to_string())),
(DataTypeId(4), DataValue::UnsignedNumber(u128::MAX)), (DataType::ServerNonce.to_id(&tm), DataValue::UnsignedNumber(u128::MAX)),
(DataTypeId(5), DataValue::Null), (DataType::PublicKeys.to_id(&tm), DataValue::Null),
]); ]);
} }
#[test] #[test]
fn test_container_nested_roundtrip() { fn test_container_nested_roundtrip() {
let tm = TypeMap::latest();
container_roundtrip(vec![ container_roundtrip(vec![
( (
DataTypeId(1), DataType::Version.to_id(&tm),
DataValue::Container(vec![(DataTypeId(10), DataValue::BoolTrue)]), 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)]), DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]),
), ),
]); ]);
@ -1095,8 +1107,9 @@ mod tests {
#[test] #[test]
fn test_container_base64_roundtrip() { fn test_container_base64_roundtrip() {
let tm = TypeMap::latest();
let dv = DataValue::Container(vec![( let dv = DataValue::Container(vec![(
DataTypeId(7), DataType::Description.to_id(&tm),
DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]), DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
)]); )]);
let b64 = dv.to_base64().expect("encode failed"); let b64 = dv.to_base64().expect("encode failed");
@ -1126,28 +1139,29 @@ mod tests {
#[test] #[test]
fn test_as_accessors() { fn test_as_accessors() {
let tm = TypeMap::latest();
let dv = DataValue::Container(vec![ let dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("alice".to_string())), (DataType::Version.to_id(&tm), DataValue::Str("alice".to_string())),
(DataTypeId(2), DataValue::SignedNumber(42)), (DataType::Id.to_id(&tm), DataValue::SignedNumber(42)),
(DataTypeId(3), DataValue::Bytes(vec![0x01, 0x02])), (DataType::ClientNonce.to_id(&tm), DataValue::Bytes(vec![0x01, 0x02])),
(DataTypeId(4), DataValue::Array(vec![DataValue::BoolTrue])), (DataType::ServerNonce.to_id(&tm), DataValue::Array(vec![DataValue::BoolTrue])),
]); ]);
let map = dv.as_map().expect("should be a container"); let map = dv.as_map().expect("should be a container");
assert_eq!( 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") Some("alice")
); );
assert_eq!( 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) Some(42)
); );
assert_eq!( 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]) Some(vec![0x01, 0x02])
); );
assert_eq!( 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]) Some(vec![DataValue::BoolTrue])
); );
} }
@ -1168,9 +1182,10 @@ mod tests {
#[test] #[test]
fn test_container_from_map() { fn test_container_from_map() {
let tm = TypeMap::latest();
let mut map = BTreeMap::new(); let mut map = BTreeMap::new();
map.insert(DataTypeId(1), DataValue::BoolTrue); map.insert(DataType::Version.to_id(&tm), DataValue::BoolTrue);
map.insert(DataTypeId(2), DataValue::SignedNumber(99)); map.insert(DataType::Id.to_id(&tm), DataValue::SignedNumber(99));
let dv = DataValue::container_from_map(&map); let dv = DataValue::container_from_map(&map);
let container = dv.as_container().expect("should be container"); let container = dv.as_container().expect("should be container");
assert_eq!(container.len(), 2); assert_eq!(container.len(), 2);
@ -1190,7 +1205,8 @@ mod tests {
#[test] #[test]
fn test_truncated_container_rejected() { 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"); let bytes = dv.to_bytes().expect("encode failed");
// Truncate to fewer than 2 bytes so neither container nor array can be read // Truncate to fewer than 2 bytes so neither container nor array can be read
assert!(DataValue::from_bytes(&bytes[..1]).is_none()); assert!(DataValue::from_bytes(&bytes[..1]).is_none());
@ -1246,9 +1262,10 @@ mod tests {
#[test] #[test]
fn test_container_display() { fn test_container_display() {
let tm = TypeMap::latest();
let dv = DataValue::Container(vec![ let dv = DataValue::Container(vec![
(DataTypeId(3), DataValue::Str("v2.0".to_string())), (DataType::ServerNonce.to_id(&tm), DataValue::Str("v2.0".to_string())),
(DataTypeId(6), DataValue::UnsignedNumber(42)), (DataType::PqSignature.to_id(&tm), DataValue::UnsignedNumber(42)),
]); ]);
let s = format!("{}", dv); let s = format!("{}", dv);
assert!(s.contains("3:")); assert!(s.contains("3:"));
@ -1268,12 +1285,13 @@ mod tests {
#[test] #[test]
fn test_encrypt_decrypt_container_roundtrip() { fn test_encrypt_decrypt_container_roundtrip() {
use mtp_crypto::{EncryptionType, Keyring}; use mtp_crypto::{EncryptionType, Keyring};
let tm = TypeMap::latest();
let keyring = Keyring::generate(); let keyring = Keyring::generate();
let bundle = keyring.public_key_bundle(); let bundle = keyring.public_key_bundle();
let mut dv = DataValue::Container(vec![ let mut dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("secret".to_string())), (DataType::Version.to_id(&tm), DataValue::Str("secret".to_string())),
(DataTypeId(2), DataValue::UnsignedNumber(42)), (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)),
]); ]);
assert!( assert!(
@ -1293,11 +1311,12 @@ mod tests {
#[test] #[test]
fn test_encrypt_container_wrong_key_fails() { fn test_encrypt_container_wrong_key_fails() {
use mtp_crypto::{EncryptionType, Keyring}; use mtp_crypto::{EncryptionType, Keyring};
let tm = TypeMap::latest();
let keyring_a = Keyring::generate(); let keyring_a = Keyring::generate();
let keyring_b = Keyring::generate(); let keyring_b = Keyring::generate();
let mut dv = 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!( assert!(
dv.encrypt_container( dv.encrypt_container(
@ -1314,10 +1333,11 @@ mod tests {
#[test] #[test]
fn test_encrypt_container_wrong_aad_fails() { fn test_encrypt_container_wrong_aad_fails() {
use mtp_crypto::{EncryptionType, Keyring}; use mtp_crypto::{EncryptionType, Keyring};
let tm = TypeMap::latest();
let keyring = Keyring::generate(); let keyring = Keyring::generate();
let mut dv = 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!( assert!(
dv.encrypt_container( dv.encrypt_container(
@ -1351,12 +1371,13 @@ mod tests {
#[test] #[test]
fn test_sign_verify_container_roundtrip() { fn test_sign_verify_container_roundtrip() {
use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, SigAlgorithm}; use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, SigAlgorithm};
let tm = TypeMap::latest();
let keyring = Keyring::generate(); let keyring = Keyring::generate();
let (signer, sk, _pk) = Ed25519Signer::generate(); let (signer, sk, _pk) = Ed25519Signer::generate();
let mut dv = DataValue::Container(vec![( let mut dv = DataValue::Container(vec![(
DataTypeId(1), DataType::Version.to_id(&tm),
DataValue::Str("signed data".to_string()), DataValue::Str("signed data".to_string()),
)]); )]);
@ -1390,13 +1411,14 @@ mod tests {
#[test] #[test]
fn test_sign_container_wrong_key_fails() { fn test_sign_container_wrong_key_fails() {
use mtp_crypto::{Ed25519Signer, SigAlgorithm}; use mtp_crypto::{Ed25519Signer, SigAlgorithm};
let tm = TypeMap::latest();
let (signer, _, _) = Ed25519Signer::generate(); let (signer, _, _) = Ed25519Signer::generate();
let (_, sk2, _) = Ed25519Signer::generate(); let (_, sk2, _) = Ed25519Signer::generate();
let wrong_verifier = Ed25519Signer::new(&sk2).unwrap(); let wrong_verifier = Ed25519Signer::new(&sk2).unwrap();
let mut dv = DataValue::Container(vec![( let mut dv = DataValue::Container(vec![(
DataTypeId(1), DataType::Version.to_id(&tm),
DataValue::Str("signed data".to_string()), DataValue::Str("signed data".to_string()),
)]); )]);

View file

@ -42,13 +42,11 @@ mod tests {
} }
} }
// =========================================================================== /* CommunicationError
// CommunicationError *
// * On native targets the full variant set (including quinn / wtransport
// On native targets the full variant set (including quinn / wtransport * wrappers) is available. On WASM only the transport-independent subset is
// wrappers) is available. On WASM only the transport-independent subset is * compiled. */
// compiled.
// ===========================================================================
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Error, Clone)] #[derive(Debug, Error, Clone)]

View file

@ -24,7 +24,7 @@ let pt = cipher.decrypt(&ct, b"aad")?;
## Signatures ## Signatures
### Ed25519 (classical) ### Ed25519
```rust ```rust
use mtp_crypto::{Ed25519Signer, SignatureScheme}; use mtp_crypto::{Ed25519Signer, SignatureScheme};
@ -34,7 +34,7 @@ let sig = signer.sign(b"message")?;
signer.verify(b"message", &sig)?; signer.verify(b"message", &sig)?;
``` ```
### ML-DSA-65 (post-quantum, requires `pqc`) ### ML-DSA-65
```rust ```rust
use mtp_crypto::{MlDsaSigner, SignatureScheme}; use mtp_crypto::{MlDsaSigner, SignatureScheme};
@ -47,7 +47,7 @@ signer.verify(b"message", &sig)?;
let signer = MlDsaSigner::new(&sk, &pk)?; let signer = MlDsaSigner::new(&sk, &pk)?;
``` ```
### Dual signatures (requires `pqc`) ### Dual signatures
```rust ```rust
use mtp_crypto::{sign_dual, DualSignature, Ed25519Signer, MlDsaSigner}; 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")?; 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. X25519 + ML-KEM-768. 64-byte shared secret. Feed into HKDF before use.

128
crypto/src/auth.rs Normal file
View file

@ -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<u8> {
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<u8> {
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<u8> {
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<u8> {
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),
);
}
}

View file

@ -16,8 +16,10 @@ pub fn hkdf_expand(
} }
pub fn hkdf_extract(ikm: &[u8], salt: &[u8]) -> [u8; 32] { 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::<Sha256>::extract(Some(salt), ikm); let (prk, _) = Hkdf::<Sha256>::extract(Some(salt), ikm);
let mut out = [0u8; 32]; let mut out = [0u8; 32];
out.copy_from_slice(&prk); out.copy_from_slice(&prk);

View file

@ -348,7 +348,7 @@ impl Keyring {
.get(*offset..*offset + 2) .get(*offset..*offset + 2)
.ok_or(CryptoError::InvalidKeyLength)? .ok_or(CryptoError::InvalidKeyLength)?
.try_into() .try_into()
.unwrap(), .expect("slice is 2 bytes, verified above"),
) as usize; ) as usize;
*offset += 2; *offset += 2;
let key = bytes let key = bytes

View file

@ -1,4 +1,5 @@
pub mod aead; pub mod aead;
pub mod auth;
pub mod error; pub mod error;
pub mod keypair; pub mod keypair;

View file

@ -1,6 +1,7 @@
use mtp::client::MTPConnection; 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::crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm};
use mtp::type_map::TypeMap;
pub fn build_demo_message( pub fn build_demo_message(
client_id: u64, client_id: u64,
@ -12,26 +13,28 @@ pub fn build_demo_message(
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key) let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
.expect("Ed25519 signer from keyring"); .expect("Ed25519 signer from keyring");
let tm = TypeMap::latest();
let inner_enc = DataValue::Container(vec![ let inner_enc = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("secret inner data".into())), (DataType::Version.to_id(&tm), DataValue::Str("secret inner data".into())),
(DataTypeId(2), DataValue::UnsignedNumber(42)), (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(42)),
]); ]);
let mut dv_enc = inner_enc; let mut dv_enc = inner_enc;
dv_enc.encrypt_container(enc_type, server_bundle, b"demo-aad"); dv_enc.encrypt_container(enc_type, server_bundle, b"demo-aad");
let inner_sig = DataValue::Container(vec![ let inner_sig = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("signed by client".into())), (DataType::Version.to_id(&tm), DataValue::Str("signed by client".into())),
(DataTypeId(2), DataValue::UnsignedNumber(99)), (DataType::Id.to_id(&tm), DataValue::UnsignedNumber(99)),
]); ]);
let mut dv_sig = inner_sig; let mut dv_sig = inner_sig;
dv_sig.sign_container(SigAlgorithm::ED25519, &signer); dv_sig.sign_container(SigAlgorithm::ED25519, &signer);
let inner_sec = DataValue::Container(vec![ let inner_sec = DataValue::Container(vec![
( (
DataTypeId(1), DataType::Version.to_id(&tm),
DataValue::Str("signed+encrypted payload".into()), 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; let mut dv_sec = inner_sec;
dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, server_bundle, b"demo-aad"); dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, server_bundle, b"demo-aad");

View file

@ -6,7 +6,7 @@ use mtp_common::CommunicationError;
use mtp_transport::{Policy, Receiver, Sender}; use mtp_transport::{Policy, Receiver, Sender};
use std::net::IpAddr; use std::net::IpAddr;
// Host configuration. /* Host configuration. */
pub struct HostConfig { pub struct HostConfig {
pub ip: IpAddr, pub ip: IpAddr,
pub port: u16, pub port: u16,
@ -33,7 +33,7 @@ pub enum AuthState {
Failed, Failed,
} }
// A connection that has completed version negotiation. /* A connection that has completed version negotiation. */
pub struct MTPConnection { pub struct MTPConnection {
pub version: Version, pub version: Version,
pub codec: VersionedCodec, pub codec: VersionedCodec,
@ -47,7 +47,7 @@ pub struct MTPConnection {
pub client_public_key: Option<mtp_crypto::PublicKeyBundle>, pub client_public_key: Option<mtp_crypto::PublicKeyBundle>,
} }
// High-level MTP host with built-in version negotiation. /* High-level MTP host with built-in version negotiation. */
pub struct MTPHost { pub struct MTPHost {
transport: mtp_transport::Host, transport: mtp_transport::Host,
registry: Registry, registry: Registry,
@ -132,221 +132,235 @@ impl MTPHost {
#[cfg(feature = "crypto")] #[cfg(feature = "crypto")]
impl MTPHost { 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( async fn accept_authenticated(
&mut self, &mut self,
sender: Sender, sender: Sender,
receiver: Receiver, receiver: Receiver,
) -> Option<MTPConnection> { ) -> Option<MTPConnection> {
use mtp_crypto::{ 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<u8>,
},
}
// 1. Receive client message first (no host greeting) let tm = TypeMap::latest();
let msg = receiver.receive().await.ok()?; let pq_enabled = !self
let version_str = match msg.get_data(DataType::Version.to_id(&tm)) { .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<u8>, Vec<u8>)> {
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(), DataValue::Str(s) => s.clone(),
_ => { _ => {
sender.close(); sender.close();
return None; return None;
} }
}; };
let client_version = Version::parse(&version_str)?; 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, DataValue::UnsignedNumber(n) => *n,
_ => { _ => {
sender.close(); sender.close();
return None; return None;
} }
}; };
let sig_bytes = match proof.get_data(DataType::Signature.to_id(&tm)) {
let sig_bytes = match msg.get_data(DataType::Signature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => { _ => {
sender.close(); sender.close();
return None; return None;
} }
}; };
let pq_sig_bytes: Vec<u8> = match proof.get_data(DataType::PqSignature.to_id(&tm)) {
let pq_sig_bytes: Vec<u8> = match msg.get_data(DataType::PqSignature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => vec![], _ => vec![],
}; };
let (assigned_id, client_bundle, response_type) = if msg.get_type() let (proof_payload, bundle) = match &flow {
== mtp_codec::CommunicationType::Identification.to_id(&tm) Flow::Login { id, bundle } => (
{ auth::login_proof_payload(&version_str, *id, server_challenge, client_nonce),
// 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,
bundle, bundle,
mtp_codec::CommunicationType::IdentificationResponse, ),
) Flow::Register {
} else if msg.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) { bundle, pk_bytes, ..
// REGISTER } => (
let bundle = match msg.get_data(DataType::PublicKeys.to_id(&tm)) { auth::register_proof_payload(
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).ok()?, &version_str,
_ => { pk_bytes,
sender.close(); server_challenge,
return None; client_nonce,
} ),
};
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,
bundle, bundle,
mtp_codec::CommunicationType::RegisterResponse, ),
)
} else {
sender.close();
return None;
}; };
// 2. Send success response (single host message) let proof_ok = verify_ed25519(&bundle.sig_cl_public_key, &proof_payload, &sig_bytes)
let new_nonce: u128 = rand::random(); .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(); if !proof_ok {
host_sig_payload.push(0x01); let rejection = CommunicationValue::new(response_type)
host_sig_payload.extend_from_slice(&assigned_id.to_be_bytes()); .add_typed_default(DataType::Connected, DataValue::BoolFalse);
host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); let _ = sender.send(&rejection).await;
host_sig_payload.extend_from_slice(&new_nonce.to_be_bytes()); 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 ===== */ // ===== Step 4: send the host's final confirmation =====
let host_sig = host_signer.sign(&host_sig_payload).ok()?; 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) let mut response = CommunicationValue::new(response_type)
.add_typed_default(DataType::Connected, DataValue::BoolTrue) .add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(assigned_id as u128))
.add_typed_default( .add_typed_default(
DataType::ClientNonce, DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce), 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::Signature, DataValue::Bytes(host_sig)) if pq_enabled {
.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()?;
response = response =
response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig)); response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig));
} }
/* ===== End Signature ===== */
sender.send(&response).await.ok()?; sender.send(&response).await.ok()?;
sender.finish_stream().await.ok()?; sender.finish_stream().await.ok()?;
// 3. Version negotiation // ===== Version negotiation =====
let negotiated = self.registry.negotiate(&[client_version])?; let negotiated = self.registry.negotiate(&[client_version])?;
let codec = VersionedCodec::new(self.registry.clone()); let codec = VersionedCodec::new(self.registry.clone());

View file

@ -1,6 +1,8 @@
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
use mtp_codec::{CommunicationType, DataType};
use mtp_transport::{Policy, connect, host}; use mtp_transport::{Policy, connect, host};
use mtp_type_map::TypeMap;
fn generate_self_signed_cert() -> (Vec<u8>, Vec<u8>) { fn generate_self_signed_cert() -> (Vec<u8>, Vec<u8>) {
let key_pair = rcgen::KeyPair::generate().unwrap(); let key_pair = rcgen::KeyPair::generate().unwrap();
@ -51,22 +53,24 @@ async fn test_send_receive_roundtrip() {
// Accept on host side // Accept on host side
let (host_tx, host_rx) = h.next().await.unwrap(); let (host_tx, host_rx) = h.next().await.unwrap();
let tm = TypeMap::latest();
// Client sends a simple message // Client sends a simple message
let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Ping).add_data( let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping).add_data(
mtp_codec::DataTypeId(6), DataType::PqSignature.to_id(&tm),
mtp_codec::DataValue::UnsignedNumber(42), mtp_codec::DataValue::UnsignedNumber(42),
); );
client_tx.send(&msg).await.unwrap(); client_tx.send(&msg).await.unwrap();
// Host receives it // Host receives it
let received = host_rx.receive().await.unwrap(); 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));
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(42)); assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(42));
// Host sends a response // Host sends a response
let resp = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Pong).add_data( let resp = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data(
mtp_codec::DataTypeId(6), DataType::PqSignature.to_id(&tm),
mtp_codec::DataValue::UnsignedNumber(99), mtp_codec::DataValue::UnsignedNumber(99),
); );
host_tx.send(&resp).await.unwrap(); host_tx.send(&resp).await.unwrap();
@ -75,9 +79,9 @@ async fn test_send_receive_roundtrip() {
let client_received = client_rx.receive().await.unwrap(); let client_received = client_rx.receive().await.unwrap();
assert_eq!( assert_eq!(
client_received.get_type(), client_received.get_type(),
mtp_codec::CommunicationTypeId(20) CommunicationType::Pong.to_id(&tm)
); // Pong );
let client_val = client_received.get_data(mtp_codec::DataTypeId(6)).clone(); let client_val = client_received.get_data(DataType::PqSignature.to_id(&tm)).clone();
assert_eq!(client_val, mtp_codec::DataValue::UnsignedNumber(99)); assert_eq!(client_val, mtp_codec::DataValue::UnsignedNumber(99));
// Close both sides // Close both sides
@ -106,10 +110,12 @@ async fn test_concurrent_messages() {
let (_host_tx, host_rx) = h.next().await.unwrap(); let (_host_tx, host_rx) = h.next().await.unwrap();
let tm = TypeMap::latest();
// Send 5 messages in sequence // Send 5 messages in sequence
for i in 0..5u128 { for i in 0..5u128 {
let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Ping).add_data( let msg = mtp_codec::CommunicationValue::new(CommunicationType::Ping).add_data(
mtp_codec::DataTypeId(6), DataType::PqSignature.to_id(&tm),
mtp_codec::DataValue::UnsignedNumber(i), mtp_codec::DataValue::UnsignedNumber(i),
); );
client_tx.send(&msg).await.unwrap(); client_tx.send(&msg).await.unwrap();
@ -118,14 +124,14 @@ async fn test_concurrent_messages() {
// Receive all 5 in order // Receive all 5 in order
for i in 0..5u128 { for i in 0..5u128 {
let received = host_rx.receive().await.unwrap(); 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)); assert_eq!(val, mtp_codec::DataValue::UnsignedNumber(i));
} }
// Send 3 responses back // Send 3 responses back
for i in 0..3u128 { for i in 0..3u128 {
let msg = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Pong).add_data( let msg = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data(
mtp_codec::DataTypeId(6), DataType::PqSignature.to_id(&tm),
mtp_codec::DataValue::UnsignedNumber(i * 10), mtp_codec::DataValue::UnsignedNumber(i * 10),
); );
client_tx.send(&msg).await.unwrap(); client_tx.send(&msg).await.unwrap();
@ -133,7 +139,7 @@ async fn test_concurrent_messages() {
for i in 0..3u128 { for i in 0..3u128 {
let received = host_rx.receive().await.unwrap(); 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)); 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(); let (_host_tx, host_rx) = h.next().await.unwrap();
// Send a message then close // 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.send(&msg).await.unwrap();
client_tx.close(); client_tx.close();
// Host should still receive the message // Host should still receive the message
let tm = TypeMap::latest();
let received = host_rx.receive().await.unwrap(); 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 // Host should get an error or closed signal on next receive
let result = host_rx.receive().await; 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(); let (host_tx, host_rx) = h.next().await.unwrap();
// Client sends a message the host receives. // 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(); client_tx.send(&msg).await.unwrap();
let _ = host_rx.receive().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. // the same connection and must keep working.
drop(host_rx); drop(host_rx);
let resp = mtp_codec::CommunicationValue::new(mtp_codec::CommunicationType::Pong).add_data( let tm = TypeMap::latest();
mtp_codec::DataTypeId(6),
let resp = mtp_codec::CommunicationValue::new(CommunicationType::Pong).add_data(
DataType::PqSignature.to_id(&tm),
mtp_codec::DataValue::UnsignedNumber(7), mtp_codec::DataValue::UnsignedNumber(7),
); );
host_tx.send(&resp).await.unwrap(); host_tx.send(&resp).await.unwrap();
let got = client_rx.receive().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!( assert_eq!(
got.get_data(mtp_codec::DataTypeId(6)).clone(), got.get_data(DataType::PqSignature.to_id(&tm)).clone(),
mtp_codec::DataValue::UnsignedNumber(7) mtp_codec::DataValue::UnsignedNumber(7)
); );

107
type-map/build.rs Normal file → Executable file
View file

@ -25,139 +25,150 @@ struct ReservedEntry {
const RESERVED_COMM_TYPES: &[ReservedEntry] = &[ const RESERVED_COMM_TYPES: &[ReservedEntry] = &[
ReservedEntry { ReservedEntry {
name: "Error", name: "Identification",
id: 0, id: 0,
}, },
ReservedEntry { ReservedEntry {
name: "ErrorParsing", name: "IdentificationResponse",
id: 1, id: 1,
}, },
ReservedEntry { ReservedEntry {
name: "ErrorBadVersion", name: "Register",
id: 2, id: 2,
}, },
ReservedEntry { ReservedEntry {
name: "Disconnect", name: "RegisterResponse",
id: 3, id: 3,
}, },
ReservedEntry { ReservedEntry {
name: "Redirect", name: "Challenge",
id: 4, id: 4,
}, },
ReservedEntry { ReservedEntry {
name: "Shutdown", name: "ChallengeResponse",
id: 5, id: 5,
}, },
ReservedEntry { ReservedEntry {
name: "BadRequest", name: "Ping",
id: 6, id: 6,
}, },
ReservedEntry { ReservedEntry {
name: "Unauthorized", name: "Pong",
id: 7, id: 7,
}, },
ReservedEntry { ReservedEntry {
name: "Forbidden", name: "Disconnect",
id: 8, id: 8,
}, },
ReservedEntry { ReservedEntry {
name: "NotFound", name: "Redirect",
id: 9, id: 9,
}, },
ReservedEntry { ReservedEntry {
name: "TooManyRequests", name: "Shutdown",
id: 10, id: 10,
}, },
ReservedEntry { ReservedEntry {
name: "InternalServerError", name: "Error",
id: 11, id: 11,
}, },
ReservedEntry { ReservedEntry {
name: "BadGateway", name: "ErrorParsing",
id: 12, id: 12,
}, },
ReservedEntry { ReservedEntry {
name: "ServiceUnavailable", name: "ErrorBadVersion",
id: 13, id: 13,
}, },
ReservedEntry { ReservedEntry {
name: "GatewayTimeout", name: "BadRequest",
id: 14, id: 14,
}, },
ReservedEntry { ReservedEntry {
name: "Identification", name: "Unauthorized",
id: 15, id: 15,
}, },
ReservedEntry { ReservedEntry {
name: "IdentificationResponse", name: "Forbidden",
id: 16, id: 16,
}, },
ReservedEntry { ReservedEntry {
name: "Register", name: "NotFound",
id: 17, id: 17,
}, },
ReservedEntry { ReservedEntry {
name: "RegisterResponse", name: "TooManyRequests",
id: 18, id: 18,
}, },
ReservedEntry { ReservedEntry {
name: "Ping", name: "InternalServerError",
id: 19, id: 19,
}, },
ReservedEntry { ReservedEntry {
name: "Pong", name: "BadGateway",
id: 20, id: 20,
}, },
ReservedEntry {
name: "ServiceUnavailable",
id: 21,
},
ReservedEntry {
name: "GatewayTimeout",
id: 22,
},
]; ];
const RESERVED_DATA_TYPES: &[ReservedEntry] = &[ const RESERVED_DATA_TYPES: &[ReservedEntry] = &[
ReservedEntry { ReservedEntry {
name: "Error", name: "Version",
id: 0, id: 0,
}, },
ReservedEntry { ReservedEntry {
name: "ErrorParsing", name: "Id",
id: 1, id: 1,
}, },
ReservedEntry { ReservedEntry {
name: "ErrorMessage", name: "ClientNonce",
id: 2, id: 2,
}, },
ReservedEntry { ReservedEntry {
name: "Version", name: "ServerNonce",
id: 3, id: 3,
}, },
ReservedEntry { ReservedEntry {
name: "Description", name: "PublicKeys",
id: 4, 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 { ReservedEntry {
name: "Signature", name: "Signature",
id: 10, id: 5,
},
ReservedEntry {
name: "Connected",
id: 11,
}, },
ReservedEntry { ReservedEntry {
name: "PqSignature", 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, id: 12,
}, },
]; ];

View file

@ -1,6 +1,8 @@
// try_new returns Result<Self, ()> deliberately: the only failure mode is "id /*
// is in the reserved range", which carries no extra information worth an error * try_new returns Result<Self, ()> deliberately: the only failure mode is "id
// type. The unit error is the intended API. * 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)] #![allow(clippy::result_unit_err)]
pub const INTERNAL_COMM_RESERVED: std::ops::Range<u16> = 0..32; pub const INTERNAL_COMM_RESERVED: std::ops::Range<u16> = 0..32;

View file

@ -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 * Verify the host's signature over the challenge it issued (step 2), mirroring
/// `0x01 || id.to_be_bytes() || client_nonce.to_be_bytes() || host_nonce.to_be_bytes()`, * the native client (`client/src/lib.rs`). `id` is the client id for a login or
/// where `id` is the client id for login and the host-assigned id for register. * `0` for a registration. The Ed25519 signature is mandatory; the ML-DSA
/// The Ed25519 signature is mandatory; the ML-DSA signature is verified only * signature is verified only when the host included one.
/// when the host included one. */
fn verify_host_signature( 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, resp: &CommunicationValue,
tm: &mtp_codec::TypeMap, tm: &mtp_codec::TypeMap,
host_pk: &mtp_crypto::PublicKeyBundle, host_pk: &mtp_crypto::PublicKeyBundle,
id: u64, id: u64,
client_nonce: u128, client_nonce: u128,
server_challenge: u128,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
let host_new_nonce = match resp.get_data(DataType::Timestamp.to_id(tm)) { if *resp.get_data(DataType::ClientNonce.to_id(tm)) != DataValue::UnsignedNumber(client_nonce) {
DataValue::UnsignedNumber(n) => *n, return Err(js_error("nonce mismatch"));
_ => return Err(js_error("missing host nonce")), }
};
let host_sig = match resp.get_data(DataType::Signature.to_id(tm)) { let host_sig = match resp.get_data(DataType::Signature.to_id(tm)) {
DataValue::Bytes(b) => b.clone(), DataValue::Bytes(b) => b.clone(),
_ => return Err(js_error("missing host signature")), _ => return Err(js_error("missing host signature")),
@ -62,12 +93,7 @@ fn verify_host_signature(
_ => vec![], _ => vec![],
}; };
let mut payload = Vec::new(); let payload = mtp_crypto::auth::host_final_payload(id, client_nonce, server_challenge);
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());
mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &host_sig) mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &host_sig)
.map_err(|_| js_error("host signature invalid"))?; .map_err(|_| js_error("host signature invalid"))?;
if !host_pq_sig.is_empty() { if !host_pq_sig.is_empty() {
@ -219,26 +245,68 @@ impl WasmClient {
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes) let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes)
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?; .map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
let tm = mtp_codec::TypeMap::latest();
let version_str = format!("{}", PROTOCOL_VERSION); 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]; let mut nonce_bytes = [0u8; 16];
getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?; getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
let client_nonce = u128::from_be_bytes(nonce_bytes); let client_nonce = u128::from_be_bytes(nonce_bytes);
// Build signature payload: version || client_id || client_nonce let proof_payload = mtp_crypto::auth::login_proof_payload(
let mut sig_payload = Vec::new(); &version_str,
sig_payload.extend_from_slice(version_str.as_bytes()); client_id,
sig_payload.extend_from_slice(&client_id.to_be_bytes()); server_challenge,
sig_payload.extend_from_slice(&client_nonce.to_be_bytes()); client_nonce,
);
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key) let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
let signature = signer let signature = signer
.sign(&sig_payload) .sign(&proof_payload)
.map_err(|e| js_error(&format!("signature failed: {}", e)))?; .map_err(|e| js_error(&format!("signature failed: {}", e)))?;
let frame = CommunicationValue::new(CommunicationType::Identification) let proof = CommunicationValue::new(CommunicationType::ChallengeResponse)
.add_typed_default(DataType::Version, DataValue::Str(version_str))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128))
.add_typed_default( .add_typed_default(
DataType::ClientNonce, DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce), DataValue::UnsignedNumber(client_nonce),
@ -246,18 +314,12 @@ impl WasmClient {
.add_typed_default(DataType::Signature, DataValue::Bytes(signature)) .add_typed_default(DataType::Signature, DataValue::Bytes(signature))
.to_bytes() .to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?; .map_err(|e| js_error(&format!("encode failed: {}", e)))?;
transport.send_frame(&proof).await?;
let transport = // 4. Receive and verify the host's final confirmation.
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
let response = transport.read_one_frame().await?; let response = transport.read_one_frame().await?;
let resp_comm = CommunicationValue::from_bytes(&response) let resp_comm = CommunicationValue::from_bytes(&response)
.map_err(|e| js_error(&format!("parse response: {}", e)))?; .map_err(|e| js_error(&format!("parse response: {}", e)))?;
let tm = mtp_codec::TypeMap::latest();
let resp_type = resp_comm.get_type(); let resp_type = resp_comm.get_type();
let expected_type = CommunicationType::IdentificationResponse.to_id(&tm); let expected_type = CommunicationType::IdentificationResponse.to_id(&tm);
if resp_type != expected_type { if resp_type != expected_type {
@ -276,15 +338,15 @@ impl WasmClient {
return Err(js_error("host rejected authentication")); return Err(js_error("host rejected authentication"));
} }
// Verify echoed nonce // Verify echoed nonce + host signature (login: id is client_id).
let echo_nonce = resp_comm.get_data(DataType::ClientNonce.to_id(&tm)); if let Err(e) = verify_host_final(
if *echo_nonce != DataValue::UnsignedNumber(client_nonce) { &resp_comm,
self.set_state(ConnectionState::Disconnected); &tm,
return Err(js_error("nonce mismatch")); &host_pk,
} client_id,
client_nonce,
// Verify the host's signature over the handshake (login: id is client_id). server_challenge,
if let Err(e) = verify_host_signature(&resp_comm, &tm, &host_pk, client_id, client_nonce) { ) {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);
return Err(e); return Err(e);
} }
@ -335,46 +397,80 @@ impl WasmClient {
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes) let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes)
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?; .map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
let tm = mtp_codec::TypeMap::latest();
let version_str = format!("{}", PROTOCOL_VERSION); 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(); 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 = let transport =
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?; WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
let inner = transport.inner().clone(); 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 response = transport.read_one_frame().await?;
let resp_comm = CommunicationValue::from_bytes(&response) let resp_comm = CommunicationValue::from_bytes(&response)
.map_err(|e| js_error(&format!("parse response: {}", e)))?; .map_err(|e| js_error(&format!("parse response: {}", e)))?;
let tm = mtp_codec::TypeMap::latest();
let resp_type = resp_comm.get_type(); let resp_type = resp_comm.get_type();
let expected_type = CommunicationType::RegisterResponse.to_id(&tm); let expected_type = CommunicationType::RegisterResponse.to_id(&tm);
if resp_type != expected_type { if resp_type != expected_type {
@ -393,12 +489,6 @@ impl WasmClient {
return Err(js_error("host rejected registration")); 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)) { let assigned_id = match resp_comm.get_data(DataType::Id.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n as u64, DataValue::UnsignedNumber(n) => *n as u64,
_ => { _ => {
@ -407,10 +497,15 @@ impl WasmClient {
} }
}; };
// Verify the host's signature over the handshake (register: id is the // Verify echoed nonce + host signature (register: id is host-assigned).
// host-assigned id). if let Err(e) = verify_host_final(
if let Err(e) = verify_host_signature(&resp_comm, &tm, &host_pk, assigned_id, client_nonce) &resp_comm,
{ &tm,
&host_pk,
assigned_id,
client_nonce,
server_challenge,
) {
self.set_state(ConnectionState::Disconnected); self.set_state(ConnectionState::Disconnected);
return Err(e); return Err(e);
} }

View file

@ -4,6 +4,7 @@ pub mod error;
pub mod message; pub mod message;
pub mod transport; pub mod transport;
#[cfg(not(test))]
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[cfg(not(test))] #[cfg(not(test))]

View file

@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*;
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue}; use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue};
use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm}; 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; use crate::error::js_error;
@ -52,10 +52,10 @@ pub fn build_demo_message(
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key) let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?; .map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
// Encrypted container (DataTypeId 1 = arbitrary custom) // Encrypted container
let inner_enc = DataValue::Container(vec![ let inner_enc = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("secret inner data".into())), (DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("secret inner data".into())),
(DataTypeId(2), DataValue::UnsignedNumber(42)), (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(42)),
]); ]);
let mut dv_enc = inner_enc; let mut dv_enc = inner_enc;
dv_enc dv_enc
@ -64,8 +64,8 @@ pub fn build_demo_message(
// Signed container // Signed container
let inner_sig = DataValue::Container(vec![ let inner_sig = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("signed by client".into())), (DataType::Version.to_id(&TypeMap::latest()), DataValue::Str("signed by client".into())),
(DataTypeId(2), DataValue::UnsignedNumber(99)), (DataType::Id.to_id(&TypeMap::latest()), DataValue::UnsignedNumber(99)),
]); ]);
let mut dv_sig = inner_sig; let mut dv_sig = inner_sig;
dv_sig dv_sig
@ -75,10 +75,10 @@ pub fn build_demo_message(
// Signed + encrypted container // Signed + encrypted container
let inner_sec = DataValue::Container(vec![ let inner_sec = DataValue::Container(vec![
( (
DataTypeId(1), DataType::Version.to_id(&TypeMap::latest()),
DataValue::Str("signed+encrypted payload".into()), 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; let mut dv_sec = inner_sec;
dv_sec dv_sec
@ -115,24 +115,24 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(response) let comm = CommunicationValue::from_bytes(response)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?; .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), DataValue::UnsignedNumber(n) => Some(*n),
_ => None, _ => 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), DataValue::UnsignedNumber(n) => Some(*n as u64),
_ => None, _ => 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), DataValue::UnsignedNumber(n) => Some(*n),
_ => None, _ => 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()), DataValue::Bytes(b) => Some(b.clone()),
_ => None, _ => None,
}; };
@ -252,15 +252,16 @@ mod tests {
fn build_ping_frame_roundtrip() { fn build_ping_frame_roundtrip() {
let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed"); let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode 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_sender(), 42);
assert_eq!( assert_eq!(
cv.get_data(DataTypeId(4)), cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("test-ping".into()) &DataValue::Str("test-ping".into())
); );
assert_eq!( assert_eq!(
cv.get_data(DataTypeId(5)), cv.get_data(DataType::Timestamp.to_id(&tm)),
&DataValue::UnsignedNumber(1234567890) &DataValue::UnsignedNumber(1234567890)
); );
} }
@ -270,16 +271,17 @@ mod tests {
let payload = b"attachment-data"; let payload = b"attachment-data";
let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed"); let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode 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_sender(), 99);
assert_eq!( assert_eq!(
cv.get_data(DataTypeId(4)), cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("with-data".into()) &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!( assert_eq!(
cv.get_data(DataTypeId(6)), cv.get_data(DataType::Id.to_id(&tm)),
&DataValue::Bytes(payload.to_vec()) &DataValue::Bytes(payload.to_vec())
); );
} }
@ -304,11 +306,12 @@ mod tests {
let bytes = result.unwrap(); let bytes = result.unwrap();
let cv = CommunicationValue::from_bytes(&bytes).expect("decode 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(), 7); assert_eq!(cv.get_sender(), 7);
assert_eq!( assert_eq!(
cv.get_data(DataTypeId(4)), cv.get_data(DataType::Description.to_id(&tm)),
&DataValue::Str("MTP WASM Demo".into()) &DataValue::Str("MTP WASM Demo".into())
); );
} }

View file

@ -223,7 +223,10 @@ impl WasmTransport {
let incoming = self.inner.incoming_unidirectional_streams(); let incoming = self.inner.incoming_unidirectional_streams();
let reader_fn = match js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader")) { let reader_fn = match js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader")) {
Ok(f) => f.dyn_into::<js_sys::Function>().unwrap(), Ok(f) => match f.dyn_into::<js_sys::Function>() {
Ok(f) => f,
Err(_) => return,
},
Err(_) => return, Err(_) => return,
}; };
let reader_val = match reader_fn.call0(&incoming) { let reader_val = match reader_fn.call0(&incoming) {
@ -233,7 +236,10 @@ impl WasmTransport {
loop { loop {
let read_fn = match js_sys::Reflect::get(&reader_val, &JsValue::from_str("read")) { let read_fn = match js_sys::Reflect::get(&reader_val, &JsValue::from_str("read")) {
Ok(f) => f.dyn_into::<js_sys::Function>().unwrap(), Ok(f) => match f.dyn_into::<js_sys::Function>() {
Ok(f) => f,
Err(_) => break,
},
Err(_) => break, Err(_) => break,
}; };
let result = match read_fn.call0(&reader_val) { let result = match read_fn.call0(&reader_val) {