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
7. Returns an `MTPConnection` with the negotiated version otherwise
### Login/Register Handshake (crypto feature)
### Login/Register Handshake
When `require_authentication` is set, the host sends a **greeting** first (host ID, public keys, nonce). The client then responds with either:
When `require_authentication` is set, the parties run a mutually-authenticated
**challenge-response**. The client speaks first with an *unsigned* hello:
- **Login** (`CommunicationType::Identification`, ID 15): client ID, nonce, signature
- **Register** (`CommunicationType::Register`, ID 17): public keys, nonce, signature
- **Login** (`CommunicationType::Identification`, ID 15): version, client ID
- **Register** (`CommunicationType::Register`, ID 17): version, public keys
The host verifies the client's signature, sends a signed response, and the client verifies the host's signature.
The host then issues a fresh random `server_challenge` in a signed `Challenge`
(`CommunicationType::Challenge`, ID 21, carrying `ServerNonce`). The client signs
that challenge, binding its id (login) or public keys (register), and returns a
`ChallengeResponse` (ID 22). The host verifies the proof against the challenge it
issued and sends a signed final response, which the client verifies.
Because the client's proof covers the host-issued `server_challenge` (a one-time
value held only on the accepting task's stack), a captured proof cannot be
replayed on another connection. All signed payloads are domain-separated; see
`mtp::crypto::auth`.
---
@ -116,8 +126,8 @@ Client (v2.0) Host (v0.0, v1.0, v2.0)
| CommValue{ Ident. } |
| Version -> "2.0" |
| Id -> 8765 |
| Nonce -> ... |
| Signature -> ... |
| (unsigned hello; auth |
| challenge follows) |
|----------------------->|
| | registry.negotiate(&[Version(2,0)])
| | -> Some(Version(2,0))

3
Cargo.lock generated
View file

@ -976,6 +976,9 @@ dependencies = [
"mtp-host",
"mtp-transport",
"mtp-type-map",
"rand 0.8.6",
"rcgen",
"tokio",
]
[[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.
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?;
```
Protocol:
1. Client generates a random nonce
2. Builds a signature payload: `version || client_id || client_nonce`
3. Signs with Ed25519 (and optionally ML-DSA-65)
4. Sends `Identification` frame containing version, client ID, nonce, signature(s)
5. Host responds with `IdentificationResponse` containing echoed nonce, host
nonce, and host signature
Protocol (challenge-response, the host issues the freshness):
1. Client sends an unsigned `Identification` hello (version, client ID)
2. Host replies with a `Challenge` carrying a fresh random `server_challenge`
and the host's signature over it; the client verifies that signature
3. Client generates a random `client_nonce` and signs
`version || client_id || server_challenge || client_nonce` with Ed25519
(and optionally ML-DSA-65)
4. Client sends a `ChallengeResponse` frame (nonce + signature(s))
5. Host verifies the proof against `server_challenge` and responds with
`IdentificationResponse` (echoed nonce + host signature)
6. Client verifies the host signature and nonce echo
Because the client's signature covers the host-issued `server_challenge`, a
captured proof cannot be replayed on another connection (each connection gets a
different challenge).
### Registration
```rust
@ -134,13 +141,16 @@ let id = conn.client_id;
let keyring_bytes = keyring.to_bytes();
```
Protocol:
1. Client generates a random nonce
2. Builds a signature payload: `version || client_nonce || public_key_bytes`
3. Signs with Ed25519 (and optionally ML-DSA-65)
4. Sends `Register` frame containing version, nonce, public key bundle, signature(s)
5. Host assigns a new client ID, responds with `RegisterResponse` containing
the ID, echoed nonce, host nonce, and host signature
Protocol (challenge-response):
1. Client sends an unsigned `Register` hello (version, public key bundle)
2. Host replies with a `Challenge` carrying a fresh random `server_challenge`
(signed by the host); the client verifies that signature
3. Client generates a random `client_nonce` and signs
`version || server_challenge || client_nonce || public_key_bytes` with
Ed25519 (and optionally ML-DSA-65)
4. Client sends a `ChallengeResponse` frame (nonce + signature(s))
5. Host verifies the proof against `server_challenge`, assigns a new client ID,
and responds with `RegisterResponse` (the ID, echoed nonce, host signature)
6. Client verifies the host signature and nonce echo
## Key Material
@ -271,7 +281,7 @@ sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, &recipi
```
On the receiving side, the recipient decrypts with its own `Keyring` (each blob
is self-describing its leading byte selects the algorithm and the matching KEM
is self-describing: its leading byte selects the algorithm and the matching KEM
key from the keyring):
```rust

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
```
## Authentication Flow (crypto feature)
## Authentication Flow
When `require_authentication` is `true`, `accept()` runs an authenticated
handshake before returning the connection. The flow is:
When `require_authentication` is `true`, `accept()` runs a mutually-authenticated
**challenge-response** handshake before returning the connection. The host issues
a fresh, random `server_challenge` that the client must sign, which is what makes
the client's proof unreplayable: a captured proof is bound to a one-time challenge
the host generates per connection and will never reissue. The challenge lives only
on the accepting task's stack; there is no replay database or shared state.
### Login (existing client)
All signed payloads begin with a one-byte domain-separation tag (see
`mtp::crypto::auth`) so a signature for one step can never be reused as another.
### Login
```
Client Host
@ -142,26 +149,35 @@ Client Host
| QUIC connect |
|---------------------------------------->|
| |
| Identification { |
| Version, Id, ClientNonce, |
| Signature, [PqSignature] |
| Identification { Version, Id } | (unsigned hello)
|---------------------------------------->|
| | lookup get_existing_user(id)
| | generate random server_challenge
| Challenge { |
| ServerNonce(server_challenge), |
| Signature, [PqSignature] | host signs the challenge
| } |
|<----------------------------------------|
| ChallengeResponse { |
| ClientNonce, Signature, [PqSignature]| client signs the challenge
| } |
|---------------------------------------->|
| | lookup get_existing_user(client_id)
| | verify Ed25519 (and optional ML-DSA) sig
| | verify proof over server_challenge
| IdentificationResponse { |
| Connected=true, ClientNonce(echoed), |
| Id, Timestamp(new_nonce), |
| Connected=true, Id, |
| ClientNonce(echoed), |
| Signature, [PqSignature] |
| } |
|<----------------------------------------|
```
The client signature payload is: `version_string || client_id (8 bytes, big-endian) || client_nonce (16 bytes, big-endian)`
Payloads (`||` is concatenation, integers big-endian; `DS_*` are domain tags):
The host signs: `0x01 || assigned_id (8 bytes, big-endian) || client_nonce (16 bytes) || host_new_nonce (16 bytes)`
- Host challenge: `DS_CHALLENGE || id (8) || server_challenge (16)`
- Client proof: `DS_LOGIN_PROOF || version_string || id (8) || server_challenge (16) || client_nonce (16)`
- Host final: `DS_HOST_FINAL || assigned_id (8) || client_nonce (16) || server_challenge (16)`
### Register (new client)
### Register
```
Client Host
@ -170,23 +186,32 @@ Client Host
|---------------------------------------->|
| |
| Register { |
| Version, ClientNonce, |
| PublicKeys (serialized PublicKeyBundle),
| Signature, [PqSignature] |
| Version, | (unsigned hello)
| PublicKeys (serialized PublicKeyBundle)
| } |
|---------------------------------------->|
| | extract PublicKeyBundle from frame
| | verify Ed25519 (and optional ML-DSA) sig
| | generate random server_challenge
| Challenge { |
| ServerNonce(server_challenge), |
| Signature, [PqSignature] | (challenge binds id = 0)
| } |
|<----------------------------------------|
| ChallengeResponse { |
| ClientNonce, Signature, [PqSignature]|
| } |
|---------------------------------------->|
| | verify proof over server_challenge
| | call complete_register(bundle) -> new_id
| RegisterResponse { |
| Connected=true, ClientNonce(echoed), |
| Id, Timestamp(new_nonce), |
| Connected=true, Id(new_id), |
| ClientNonce(echoed), |
| Signature, [PqSignature] |
| } |
|<----------------------------------------|
```
The client signature payload is: `version_string || client_nonce (16 bytes) || public_key_bytes`
The register client proof is:
`DS_REGISTER_PROOF || version_string || server_challenge (16) || client_nonce (16) || public_key_bytes`
After a successful handshake, `accept()` returns an `MTPConnection` with
`auth_state = Authenticated`, `client_id` set, and `client_public_key`

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.
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

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.
### Authenticated Login (existing client ID)
### Authenticated Login
```typescript
const confirmedId = await client.auth_connect(
@ -108,10 +108,14 @@ const confirmedId = await client.auth_connect(
);
```
Exchange: client sends a signed `Identification` frame, the host verifies it and
responds with a signed `IdentificationResponse`. Returns the confirmed client ID.
Exchange (challenge-response): the client sends an unsigned `Identification`
hello, the host replies with a signed `Challenge` carrying a fresh
`server_challenge`, the client signs that challenge in a `ChallengeResponse`, and
the host verifies it and replies with a signed `IdentificationResponse`. Signing
over the host-issued challenge is what prevents a captured proof from being
replayed on another connection. Returns the confirmed client ID.
### Registration (new client)
### Registration
```typescript
const newId = await client.auth_register(
@ -121,8 +125,10 @@ const newId = await client.auth_register(
);
```
Exchange: client sends a signed `Register` frame with public keys, the host
assigns a new ID and responds with a signed `RegisterResponse`. Returns the
Exchange (challenge-response): the client sends an unsigned `Register` hello with
its public keys, the host replies with a signed `Challenge`, the client signs it
(binding the public-key bundle) in a `ChallengeResponse`, and the host verifies
it, assigns a new ID, and responds with a signed `RegisterResponse`. Returns the
newly assigned client ID.
## Sending and Receiving Messages

View file

@ -22,7 +22,7 @@ pub struct ClientConfig {
pub client_id: u64,
}
// Established MTP connection with a single negotiated version.
/* Established MTP connection with a single negotiated version. */
pub struct MTPConnection {
pub version: Version,
pub sender: Sender,
@ -78,6 +78,113 @@ impl MTPClient {
}
/* ===== Authentication ===== */
/*
* Verify the host's signature over the challenge it issued (step 2).
*
* `id` is the client id for a login, or `0` for a registration (the host binds
* `0` since no id has been assigned yet). The Ed25519 signature is mandatory;
* the ML-DSA signature is checked only when the host included one.
*/
#[cfg(feature = "crypto")]
fn verify_host_challenge(
challenge: &CommunicationValue,
tm: &mtp_codec::TypeMap,
host_pk: &mtp_crypto::PublicKeyBundle,
id: u64,
server_challenge: u128,
) -> Result<(), CommunicationError> {
use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa};
let sig = match challenge.get_data(DataType::Signature.to_id(tm)) {
DataValue::Bytes(b) => b.clone(),
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing host challenge signature".into(),
));
}
};
let pq_sig = match challenge.get_data(DataType::PqSignature.to_id(tm)) {
DataValue::Bytes(b) => b.clone(),
_ => vec![],
};
let payload = auth::challenge_payload(id, server_challenge);
verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig).map_err(|_| {
CommunicationError::AuthenticationFailed("Host challenge signature invalid".into())
})?;
if !pq_sig.is_empty() && verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig).is_err() {
return Err(CommunicationError::AuthenticationFailed(
"Host challenge PQ signature invalid".into(),
));
}
Ok(())
}
/*
* Verify the host's final confirmation (step 4): the echoed `client_nonce` and
* the host signature over the handshake transcript.
*/
#[cfg(feature = "crypto")]
fn verify_host_final(
response: &CommunicationValue,
tm: &mtp_codec::TypeMap,
host_pk: &mtp_crypto::PublicKeyBundle,
id: u64,
client_nonce: u128,
server_challenge: u128,
) -> Result<(), CommunicationError> {
use mtp_crypto::{auth, verify_ed25519, verify_ml_dsa};
match response.get_data(DataType::ClientNonce.to_id(tm)) {
DataValue::UnsignedNumber(n) if *n == client_nonce => {}
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Nonce mismatch".into(),
));
}
}
let sig = match response.get_data(DataType::Signature.to_id(tm)) {
DataValue::Bytes(b) => b.clone(),
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing signature".into(),
));
}
};
let pq_sig = match response.get_data(DataType::PqSignature.to_id(tm)) {
DataValue::Bytes(b) => b.clone(),
_ => vec![],
};
let payload = auth::host_final_payload(id, client_nonce, server_challenge);
verify_ed25519(&host_pk.sig_cl_public_key, &payload, &sig)
.map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?;
if !pq_sig.is_empty() && verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &pq_sig).is_err() {
return Err(CommunicationError::AuthenticationFailed(
"Host PQ signature invalid".into(),
));
}
Ok(())
}
/* Interpret the host's `Connected` flag. */
#[cfg(feature = "crypto")]
fn check_connected(
response: &CommunicationValue,
tm: &mtp_codec::TypeMap,
reject_msg: &str,
) -> Result<(), CommunicationError> {
match response.get_data(DataType::Connected.to_id(tm)) {
DataValue::BoolTrue => Ok(()),
DataValue::BoolFalse => Err(CommunicationError::AuthenticationFailed(reject_msg.into())),
_ => Err(CommunicationError::AuthenticationFailed(
"Invalid response".into(),
)),
}
}
#[cfg(feature = "crypto")]
impl MTPClient {
pub async fn auth_connect(
@ -85,59 +192,82 @@ impl MTPClient {
keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> {
use mtp_crypto::{
Ed25519Signer, MlDsaSigner, SignatureScheme, verify_ed25519, verify_ml_dsa,
};
use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth};
let (sender, receiver) =
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
// 1. Build and send Identification message immediately (no greeting)
let client_nonce: u128 = rand::random();
let tm = mtp_codec::TypeMap::latest();
let version_str = format!("{}", PROTOCOL_VERSION);
let mut sig_payload = Vec::new();
sig_payload.extend_from_slice(version_str.as_bytes());
sig_payload.extend_from_slice(&config.client_id.to_be_bytes());
sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
let signer = Ed25519Signer::new(&keys.sig_cl_secret_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
/* ===== Signature ===== */
let signature = signer
.sign(&sig_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
.add_typed_default(DataType::Version, DataValue::Str(version_str))
// 1. Send the unsigned Identification hello (version + claimed id).
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
.add_typed_default(
DataType::Id,
DataValue::UnsignedNumber(config.client_id as u128),
)
);
sender.send(&ident).await?;
// 2. Receive and verify the host's challenge.
let challenge = receiver.receive().await?;
let expected = mtp_codec::CommunicationType::Challenge.to_id(&tm);
if challenge.get_type() != expected {
return Err(unexpected_response_type_error(
"auth_connect challenge",
expected,
&challenge,
));
}
let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n,
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing server challenge".into(),
));
}
};
verify_host_challenge(
&challenge,
&tm,
host_public_key_bundle,
config.client_id,
server_challenge,
)?;
// 3. Sign the host's challenge and send the proof.
let client_nonce: u128 = rand::random();
let proof_payload = auth::login_proof_payload(
&version_str,
config.client_id,
server_challenge,
client_nonce,
);
let signer = Ed25519Signer::new(&keys.sig_cl_secret_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
let signature = signer
.sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
)
.add_typed_default(DataType::Signature, DataValue::Bytes(signature));
if !keys.sig_pq_secret_key.as_bytes().is_empty() {
let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
let pq_signature = pq_signer
.sign(&sig_payload)
.sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
ident = ident.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
}
/* ===== End Signature ===== */
sender.send(&proof).await?;
sender.send(&ident).await?;
// 2. Receive host response (single message)
// 4. Receive and verify the host's final confirmation.
let response = receiver.receive().await?;
let tm = mtp_codec::TypeMap::latest();
let expected_type = mtp_codec::CommunicationType::IdentificationResponse.to_id(&tm);
if response.get_type() != expected_type {
return Err(unexpected_response_type_error(
@ -146,81 +276,15 @@ impl MTPClient {
&response,
));
}
let connected = response.get_data(DataType::Connected.to_id(&tm));
match connected {
DataValue::BoolTrue => {}
DataValue::BoolFalse => {
return Err(CommunicationError::AuthenticationFailed(
"Server rejected authentication".into(),
));
}
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Invalid response".into(),
));
}
}
let echo_nonce = response.get_data(DataType::ClientNonce.to_id(&tm));
match echo_nonce {
DataValue::UnsignedNumber(n) if *n == client_nonce as u128 => {}
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Nonce mismatch".into(),
));
}
}
let host_new_nonce = match response.get_data(DataType::Timestamp.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n,
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing new nonce".into(),
));
}
};
let host_sig = match response.get_data(DataType::Signature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(),
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing signature".into(),
));
}
};
let host_pq_sig = match response.get_data(DataType::PqSignature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(),
_ => vec![],
};
let mut host_sig_payload = Vec::new();
host_sig_payload.push(0x01);
host_sig_payload.extend_from_slice(&config.client_id.to_be_bytes());
host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
host_sig_payload.extend_from_slice(&host_new_nonce.to_be_bytes());
/* ===== Signature ===== */
verify_ed25519(
&host_public_key_bundle.sig_cl_public_key,
&host_sig_payload,
&host_sig,
)
.map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?;
if !host_pq_sig.is_empty()
&& verify_ml_dsa(
&host_public_key_bundle.sig_pq_public_key,
&host_sig_payload,
&host_pq_sig,
)
.is_err()
{
return Err(CommunicationError::AuthenticationFailed(
"Host PQ signature invalid".into(),
));
}
/* ===== End Signature ===== */
check_connected(&response, &tm, "Server rejected authentication")?;
verify_host_final(
&response,
&tm,
host_public_key_bundle,
config.client_id,
client_nonce,
server_challenge,
)?;
Ok(MTPConnection {
version: PROTOCOL_VERSION,
@ -236,59 +300,72 @@ impl MTPClient {
keys: &mtp_crypto::Keyring,
host_public_key_bundle: &mtp_crypto::PublicKeyBundle,
) -> Result<MTPConnection, CommunicationError> {
use mtp_crypto::{
Ed25519Signer, MlDsaSigner, SignatureScheme, verify_ed25519, verify_ml_dsa,
};
use mtp_crypto::{Ed25519Signer, MlDsaSigner, SignatureScheme, auth};
let (sender, receiver) =
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
// 1. Build and send Register message immediately
let client_nonce: u128 = rand::random();
let tm = mtp_codec::TypeMap::latest();
let version_str = format!("{}", PROTOCOL_VERSION);
let pk_bundle = keys.public_key_bundle();
let pk_bytes = pk_bundle.as_bytes();
let mut sig_payload = Vec::new();
sig_payload.extend_from_slice(version_str.as_bytes());
sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
sig_payload.extend_from_slice(&pk_bytes);
// 1. Send the unsigned Register hello (version + public-key bundle).
let register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
sender.send(&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)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
/* ===== Signature ===== */
let signature = signer
.sign(&sig_payload)
.sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
let mut register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
.add_typed_default(DataType::Version, DataValue::Str(version_str))
let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
)
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes))
.add_typed_default(DataType::Signature, DataValue::Bytes(signature));
if !keys.sig_pq_secret_key.as_bytes().is_empty() {
let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
let pq_signature = pq_signer
.sign(&sig_payload)
.sign(&proof_payload)
.map_err(|e| CommunicationError::Other(e.to_string()))?;
register =
register.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
}
/* ===== End Signature ===== */
sender.send(&proof).await?;
sender.send(&register).await?;
// 2. Receive host response (single message)
// 4. Receive the host's final confirmation; extract the assigned id and
// verify the host signature binds to it.
let response = receiver.receive().await?;
let tm = mtp_codec::TypeMap::latest();
let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm);
if response.get_type() != expected_type {
return Err(unexpected_response_type_error(
@ -297,97 +374,30 @@ impl MTPClient {
&response,
));
}
let connected = response.get_data(DataType::Connected.to_id(&tm));
match connected {
DataValue::BoolTrue => {}
DataValue::BoolFalse => {
return Err(CommunicationError::AuthenticationFailed(
"Server rejected registration".into(),
));
}
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Invalid response".into(),
));
}
}
check_connected(&response, &tm, "Server rejected registration")?;
let assigned_id = match response.get_data(DataType::Id.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n,
DataValue::UnsignedNumber(n) => *n as u64,
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing assigned ID".into(),
));
}
};
let echo_nonce = response.get_data(DataType::ClientNonce.to_id(&tm));
match echo_nonce {
DataValue::UnsignedNumber(n) if *n == client_nonce as u128 => {}
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Nonce mismatch".into(),
));
}
}
let host_new_nonce = match response.get_data(DataType::Timestamp.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n,
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing new nonce".into(),
));
}
};
let host_sig = match response.get_data(DataType::Signature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(),
_ => {
return Err(CommunicationError::AuthenticationFailed(
"Missing signature".into(),
));
}
};
let host_pq_sig = match response.get_data(DataType::PqSignature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(),
_ => vec![],
};
let mut host_sig_payload = Vec::new();
host_sig_payload.push(0x01);
host_sig_payload.extend_from_slice(&(assigned_id as u64).to_be_bytes());
host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
host_sig_payload.extend_from_slice(&host_new_nonce.to_be_bytes());
/* ===== Signature ===== */
verify_ed25519(
&host_public_key_bundle.sig_cl_public_key,
&host_sig_payload,
&host_sig,
)
.map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?;
if !host_pq_sig.is_empty()
&& verify_ml_dsa(
&host_public_key_bundle.sig_pq_public_key,
&host_sig_payload,
&host_pq_sig,
)
.is_err()
{
return Err(CommunicationError::AuthenticationFailed(
"Host PQ signature invalid".into(),
));
}
/* ===== End Signature ===== */
verify_host_final(
&response,
&tm,
host_public_key_bundle,
assigned_id,
client_nonce,
server_challenge,
)?;
Ok(MTPConnection {
version: PROTOCOL_VERSION,
sender,
receiver,
auth_state: AuthState::Authenticated,
client_id: assigned_id as u64,
client_id: assigned_id,
})
}
}

View file

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

View file

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

View file

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

View file

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

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] {
// 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 mut out = [0u8; 32];
out.copy_from_slice(&prk);

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ use mtp_common::CommunicationError;
use mtp_transport::{Policy, Receiver, Sender};
use std::net::IpAddr;
// Host configuration.
/* Host configuration. */
pub struct HostConfig {
pub ip: IpAddr,
pub port: u16,
@ -33,7 +33,7 @@ pub enum AuthState {
Failed,
}
// A connection that has completed version negotiation.
/* A connection that has completed version negotiation. */
pub struct MTPConnection {
pub version: Version,
pub codec: VersionedCodec,
@ -47,7 +47,7 @@ pub struct MTPConnection {
pub client_public_key: Option<mtp_crypto::PublicKeyBundle>,
}
// High-level MTP host with built-in version negotiation.
/* High-level MTP host with built-in version negotiation. */
pub struct MTPHost {
transport: mtp_transport::Host,
registry: Registry,
@ -132,221 +132,235 @@ impl MTPHost {
#[cfg(feature = "crypto")]
impl MTPHost {
/*
* Mutually-authenticated handshake with a server-issued challenge.
*
* 1. C -> H : Identification { version, id } (or Register { version, public_keys })
* 2. H -> C : Challenge { server_challenge, host_sig }
* 3. C -> H : ChallengeResponse { client_nonce, sig }
* 4. H -> C : IdentificationResponse / RegisterResponse { connected, id, sig }
*
* The client's authenticating signature (step 3) covers `server_challenge`,
* a fresh value generated here in step 2 and kept on this task's stack for
* the lifetime of the connection. It is therefore one-time per connection
* with no shared replay state, and a captured proof cannot be replayed on
* any other connection.
*/
async fn accept_authenticated(
&mut self,
sender: Sender,
receiver: Receiver,
) -> Option<MTPConnection> {
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 msg = receiver.receive().await.ok()?;
let version_str = match msg.get_data(DataType::Version.to_id(&tm)) {
let tm = TypeMap::latest();
let pq_enabled = !self
.config
.host_keyring
.sig_pq_secret_key
.as_bytes()
.is_empty();
// Sign `payload` with the host keys (Ed25519 always, ML-DSA when configured).
let host_sign = |payload: &[u8]| -> Option<(Vec<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(),
_ => {
sender.close();
return None;
}
};
let client_version = Version::parse(&version_str)?;
let client_nonce = match msg.get_data(DataType::ClientNonce.to_id(&tm)) {
let (flow, response_type) =
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
// LOGIN: look up the claimed user before issuing a challenge.
let cid = match hello.get_data(DataType::Id.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n as u64,
_ => {
sender.close();
return None;
}
};
let bundle = match (self.config.get_existing_user)(cid) {
Some(b) => b,
None => {
let rejection = CommunicationValue::new(
mtp_codec::CommunicationType::IdentificationResponse,
)
.add_typed_default(DataType::Connected, DataValue::BoolFalse);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
};
(
Flow::Login { id: cid, bundle },
mtp_codec::CommunicationType::IdentificationResponse,
)
} else if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
// REGISTER: the client presents the bundle it wants to register.
let bundle = match hello.get_data(DataType::PublicKeys.to_id(&tm)) {
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).ok()?,
_ => {
sender.close();
return None;
}
};
let pk_bytes = bundle.as_bytes();
(
Flow::Register { bundle, pk_bytes },
mtp_codec::CommunicationType::RegisterResponse,
)
} else {
sender.close();
return None;
};
// The id bound into the challenge (0 for register: none assigned yet).
let challenge_id = match &flow {
Flow::Login { id, .. } => *id,
Flow::Register { .. } => 0,
};
// ===== Step 2: issue a fresh, host-signed challenge =====
let server_challenge: u128 = rand::random();
let (chal_sig, chal_pq_sig) =
host_sign(&auth::challenge_payload(challenge_id, server_challenge))?;
let mut challenge_msg = CommunicationValue::new(mtp_codec::CommunicationType::Challenge)
.add_typed_default(
DataType::ServerNonce,
DataValue::UnsignedNumber(server_challenge),
)
.add_typed_default(DataType::Signature, DataValue::Bytes(chal_sig));
if pq_enabled {
challenge_msg = challenge_msg
.add_typed_default(DataType::PqSignature, DataValue::Bytes(chal_pq_sig));
}
sender.send(&challenge_msg).await.ok()?;
// ===== Step 3: receive and verify the client's proof =====
let proof = receiver.receive().await.ok()?;
if proof.get_type() != mtp_codec::CommunicationType::ChallengeResponse.to_id(&tm) {
sender.close();
return None;
}
let client_nonce = match proof.get_data(DataType::ClientNonce.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n,
_ => {
sender.close();
return None;
}
};
let sig_bytes = match msg.get_data(DataType::Signature.to_id(&tm)) {
let sig_bytes = match proof.get_data(DataType::Signature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(),
_ => {
sender.close();
return None;
}
};
let pq_sig_bytes: Vec<u8> = match msg.get_data(DataType::PqSignature.to_id(&tm)) {
let pq_sig_bytes: Vec<u8> = match proof.get_data(DataType::PqSignature.to_id(&tm)) {
DataValue::Bytes(b) => b.clone(),
_ => vec![],
};
let (assigned_id, client_bundle, response_type) = if msg.get_type()
== mtp_codec::CommunicationType::Identification.to_id(&tm)
{
// LOGIN
let cid = match msg.get_data(DataType::Id.to_id(&tm)) {
DataValue::UnsignedNumber(n) => *n as u64,
_ => {
sender.close();
return None;
}
};
let bundle = match (self.config.get_existing_user)(cid) {
Some(b) => b,
None => {
let rejection = CommunicationValue::new(
mtp_codec::CommunicationType::IdentificationResponse,
)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
};
let mut sig_payload = Vec::new();
sig_payload.extend_from_slice(version_str.as_bytes());
sig_payload.extend_from_slice(&cid.to_be_bytes());
sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
/* ===== Signature ===== */
if verify_ed25519(&bundle.sig_cl_public_key, &sig_payload, &sig_bytes).is_err() {
let rejection =
CommunicationValue::new(mtp_codec::CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
if !pq_sig_bytes.is_empty()
&& verify_ml_dsa(&bundle.sig_pq_public_key, &sig_payload, &pq_sig_bytes).is_err()
{
let rejection =
CommunicationValue::new(mtp_codec::CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
/* ===== End Signature ===== */
(
cid,
let (proof_payload, bundle) = match &flow {
Flow::Login { id, bundle } => (
auth::login_proof_payload(&version_str, *id, server_challenge, client_nonce),
bundle,
mtp_codec::CommunicationType::IdentificationResponse,
)
} else if msg.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
// REGISTER
let bundle = match msg.get_data(DataType::PublicKeys.to_id(&tm)) {
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).ok()?,
_ => {
sender.close();
return None;
}
};
let pk_bytes = bundle.as_bytes();
let mut sig_payload = Vec::new();
sig_payload.extend_from_slice(version_str.as_bytes());
sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
sig_payload.extend_from_slice(&pk_bytes);
/* ===== Signature ===== */
if verify_ed25519(&bundle.sig_cl_public_key, &sig_payload, &sig_bytes).is_err() {
let rejection =
CommunicationValue::new(mtp_codec::CommunicationType::RegisterResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
if !pq_sig_bytes.is_empty()
&& verify_ml_dsa(&bundle.sig_pq_public_key, &sig_payload, &pq_sig_bytes).is_err()
{
let rejection =
CommunicationValue::new(mtp_codec::CommunicationType::RegisterResponse)
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
/* ===== End Signature ===== */
let new_id = (self.config.complete_register)(bundle.clone());
(
new_id,
),
Flow::Register {
bundle, pk_bytes, ..
} => (
auth::register_proof_payload(
&version_str,
pk_bytes,
server_challenge,
client_nonce,
),
bundle,
mtp_codec::CommunicationType::RegisterResponse,
)
} else {
sender.close();
return None;
),
};
// 2. Send success response (single host message)
let new_nonce: u128 = rand::random();
let proof_ok = verify_ed25519(&bundle.sig_cl_public_key, &proof_payload, &sig_bytes)
.is_ok()
&& (pq_sig_bytes.is_empty()
|| verify_ml_dsa(&bundle.sig_pq_public_key, &proof_payload, &pq_sig_bytes).is_ok());
let mut host_sig_payload = Vec::new();
host_sig_payload.push(0x01);
host_sig_payload.extend_from_slice(&assigned_id.to_be_bytes());
host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
host_sig_payload.extend_from_slice(&new_nonce.to_be_bytes());
if !proof_ok {
let rejection = CommunicationValue::new(response_type)
.add_typed_default(DataType::Connected, DataValue::BoolFalse);
let _ = sender.send(&rejection).await;
sender.close();
return None;
}
let host_signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key).ok()?;
// Proof verified: resolve the assigned id and retain the client's bundle.
let (assigned_id, client_bundle) = match flow {
Flow::Login { id, bundle } => (id, bundle),
Flow::Register { bundle, .. } => {
let new_id = (self.config.complete_register)(bundle.clone());
(new_id, bundle)
}
};
/* ===== Signature ===== */
let host_sig = host_signer.sign(&host_sig_payload).ok()?;
// ===== Step 4: send the host's final confirmation =====
let (host_sig, host_pq_sig) = host_sign(&auth::host_final_payload(
assigned_id,
client_nonce,
server_challenge,
))?;
let mut response = CommunicationValue::new(response_type)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(assigned_id as u128))
.add_typed_default(
DataType::ClientNonce,
DataValue::UnsignedNumber(client_nonce),
)
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(new_nonce))
.add_typed_default(DataType::Signature, DataValue::Bytes(host_sig))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(assigned_id as u128));
if !self
.config
.host_keyring
.sig_pq_secret_key
.as_bytes()
.is_empty()
{
use mtp_crypto::MlDsaSigner;
let host_pq_signer = MlDsaSigner::new(
&self.config.host_keyring.sig_pq_secret_key,
&self.config.host_keyring.sig_pq_public_key,
)
.ok()?;
let host_pq_sig = host_pq_signer.sign(&host_sig_payload).ok()?;
.add_typed_default(DataType::Signature, DataValue::Bytes(host_sig));
if pq_enabled {
response =
response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig));
}
/* ===== End Signature ===== */
sender.send(&response).await.ok()?;
sender.finish_stream().await.ok()?;
// 3. Version negotiation
// ===== Version negotiation =====
let negotiated = self.registry.negotiate(&[client_version])?;
let codec = VersionedCodec::new(self.registry.clone());

View file

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

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

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

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

View file

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

View file

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

View file

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