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:
parent
687e6f9642
commit
f4118f28ba
25 changed files with 1032 additions and 667 deletions
24
CONNECTOR.md
24
CONNECTOR.md
|
|
@ -68,14 +68,24 @@ The host's `accept()` method:
|
|||
6. Returns `None` if the version is unsupported
|
||||
7. Returns an `MTPConnection` with the negotiated version otherwise
|
||||
|
||||
### Login/Register Handshake (crypto feature)
|
||||
### Login/Register Handshake
|
||||
|
||||
When `require_authentication` is set, the host sends a **greeting** first (host ID, public keys, nonce). The client then responds with either:
|
||||
When `require_authentication` is set, the parties run a mutually-authenticated
|
||||
**challenge-response**. The client speaks first with an *unsigned* hello:
|
||||
|
||||
- **Login** (`CommunicationType::Identification`, ID 15): client ID, nonce, signature
|
||||
- **Register** (`CommunicationType::Register`, ID 17): public keys, nonce, signature
|
||||
- **Login** (`CommunicationType::Identification`, ID 15): version, client ID
|
||||
- **Register** (`CommunicationType::Register`, ID 17): version, public keys
|
||||
|
||||
The host verifies the client's signature, sends a signed response, and the client verifies the host's signature.
|
||||
The host then issues a fresh random `server_challenge` in a signed `Challenge`
|
||||
(`CommunicationType::Challenge`, ID 21, carrying `ServerNonce`). The client signs
|
||||
that challenge, binding its id (login) or public keys (register), and returns a
|
||||
`ChallengeResponse` (ID 22). The host verifies the proof against the challenge it
|
||||
issued and sends a signed final response, which the client verifies.
|
||||
|
||||
Because the client's proof covers the host-issued `server_challenge` (a one-time
|
||||
value held only on the accepting task's stack), a captured proof cannot be
|
||||
replayed on another connection. All signed payloads are domain-separated; see
|
||||
`mtp::crypto::auth`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -116,8 +126,8 @@ Client (v2.0) Host (v0.0, v1.0, v2.0)
|
|||
| CommValue{ Ident. } |
|
||||
| Version -> "2.0" |
|
||||
| Id -> 8765 |
|
||||
| Nonce -> ... |
|
||||
| Signature -> ... |
|
||||
| (unsigned hello; auth |
|
||||
| challenge follows) |
|
||||
|----------------------->|
|
||||
| | registry.negotiate(&[Version(2,0)])
|
||||
| | -> Some(Version(2,0))
|
||||
|
|
|
|||
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -976,6 +976,9 @@ dependencies = [
|
|||
"mtp-host",
|
||||
"mtp-transport",
|
||||
"mtp-type-map",
|
||||
"rand 0.8.6",
|
||||
"rcgen",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(®ister).await?;
|
||||
|
||||
// 2. Receive and verify the host's challenge (register binds id = 0).
|
||||
let challenge = receiver.receive().await?;
|
||||
let expected = mtp_codec::CommunicationType::Challenge.to_id(&tm);
|
||||
if challenge.get_type() != expected {
|
||||
return Err(unexpected_response_type_error(
|
||||
"auth_register challenge",
|
||||
expected,
|
||||
&challenge,
|
||||
));
|
||||
}
|
||||
let server_challenge = match challenge.get_data(DataType::ServerNonce.to_id(&tm)) {
|
||||
DataValue::UnsignedNumber(n) => *n,
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Missing server challenge".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
verify_host_challenge(&challenge, &tm, host_public_key_bundle, 0, server_challenge)?;
|
||||
|
||||
// 3. Sign the host's challenge over the bundle and send the proof.
|
||||
let client_nonce: u128 = rand::random();
|
||||
let proof_payload =
|
||||
auth::register_proof_payload(&version_str, &pk_bytes, server_challenge, client_nonce);
|
||||
|
||||
let signer = Ed25519Signer::new(&keys.sig_cl_secret_key)
|
||||
.map_err(|e| CommunicationError::Other(e.to_string()))?;
|
||||
|
||||
/* ===== Signature ===== */
|
||||
let signature = signer
|
||||
.sign(&sig_payload)
|
||||
.sign(&proof_payload)
|
||||
.map_err(|e| CommunicationError::Other(e.to_string()))?;
|
||||
|
||||
let mut register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
let mut proof = CommunicationValue::new(mtp_codec::CommunicationType::ChallengeResponse)
|
||||
.add_typed_default(
|
||||
DataType::ClientNonce,
|
||||
DataValue::UnsignedNumber(client_nonce),
|
||||
)
|
||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes))
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature));
|
||||
|
||||
if !keys.sig_pq_secret_key.as_bytes().is_empty() {
|
||||
let pq_signer = MlDsaSigner::new(&keys.sig_pq_secret_key, &keys.sig_pq_public_key)
|
||||
.map_err(|e| CommunicationError::Other(e.to_string()))?;
|
||||
let pq_signature = pq_signer
|
||||
.sign(&sig_payload)
|
||||
.sign(&proof_payload)
|
||||
.map_err(|e| CommunicationError::Other(e.to_string()))?;
|
||||
register =
|
||||
register.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
|
||||
proof = proof.add_typed_default(DataType::PqSignature, DataValue::Bytes(pq_signature));
|
||||
}
|
||||
/* ===== End Signature ===== */
|
||||
sender.send(&proof).await?;
|
||||
|
||||
sender.send(®ister).await?;
|
||||
|
||||
// 2. Receive host response (single message)
|
||||
// 4. Receive the host's final confirmation; extract the assigned id and
|
||||
// verify the host signature binds to it.
|
||||
let response = receiver.receive().await?;
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
|
||||
let expected_type = mtp_codec::CommunicationType::RegisterResponse.to_id(&tm);
|
||||
if response.get_type() != expected_type {
|
||||
return Err(unexpected_response_type_error(
|
||||
|
|
@ -297,97 +374,30 @@ impl MTPClient {
|
|||
&response,
|
||||
));
|
||||
}
|
||||
|
||||
let connected = response.get_data(DataType::Connected.to_id(&tm));
|
||||
match connected {
|
||||
DataValue::BoolTrue => {}
|
||||
DataValue::BoolFalse => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Server rejected registration".into(),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Invalid response".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
check_connected(&response, &tm, "Server rejected registration")?;
|
||||
let assigned_id = match response.get_data(DataType::Id.to_id(&tm)) {
|
||||
DataValue::UnsignedNumber(n) => *n,
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Missing assigned ID".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let echo_nonce = response.get_data(DataType::ClientNonce.to_id(&tm));
|
||||
match echo_nonce {
|
||||
DataValue::UnsignedNumber(n) if *n == client_nonce as u128 => {}
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Nonce mismatch".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let host_new_nonce = match response.get_data(DataType::Timestamp.to_id(&tm)) {
|
||||
DataValue::UnsignedNumber(n) => *n,
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Missing new nonce".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let host_sig = match response.get_data(DataType::Signature.to_id(&tm)) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => {
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Missing signature".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let host_pq_sig = match response.get_data(DataType::PqSignature.to_id(&tm)) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let mut host_sig_payload = Vec::new();
|
||||
host_sig_payload.push(0x01);
|
||||
host_sig_payload.extend_from_slice(&(assigned_id as u64).to_be_bytes());
|
||||
host_sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
|
||||
host_sig_payload.extend_from_slice(&host_new_nonce.to_be_bytes());
|
||||
|
||||
/* ===== Signature ===== */
|
||||
verify_ed25519(
|
||||
&host_public_key_bundle.sig_cl_public_key,
|
||||
&host_sig_payload,
|
||||
&host_sig,
|
||||
)
|
||||
.map_err(|_| CommunicationError::AuthenticationFailed("Host signature invalid".into()))?;
|
||||
if !host_pq_sig.is_empty()
|
||||
&& verify_ml_dsa(
|
||||
&host_public_key_bundle.sig_pq_public_key,
|
||||
&host_sig_payload,
|
||||
&host_pq_sig,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Err(CommunicationError::AuthenticationFailed(
|
||||
"Host PQ signature invalid".into(),
|
||||
));
|
||||
}
|
||||
/* ===== End Signature ===== */
|
||||
verify_host_final(
|
||||
&response,
|
||||
&tm,
|
||||
host_public_key_bundle,
|
||||
assigned_id,
|
||||
client_nonce,
|
||||
server_challenge,
|
||||
)?;
|
||||
|
||||
Ok(MTPConnection {
|
||||
version: PROTOCOL_VERSION,
|
||||
sender,
|
||||
receiver,
|
||||
auth_state: AuthState::Authenticated,
|
||||
client_id: assigned_id as u64,
|
||||
client_id: assigned_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
128
crypto/src/auth.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod aead;
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
pub mod keypair;
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
342
host/src/lib.rs
342
host/src/lib.rs
|
|
@ -6,7 +6,7 @@ use mtp_common::CommunicationError;
|
|||
use mtp_transport::{Policy, Receiver, Sender};
|
||||
use std::net::IpAddr;
|
||||
|
||||
// Host configuration.
|
||||
/* Host configuration. */
|
||||
pub struct HostConfig {
|
||||
pub ip: IpAddr,
|
||||
pub port: u16,
|
||||
|
|
@ -33,7 +33,7 @@ pub enum AuthState {
|
|||
Failed,
|
||||
}
|
||||
|
||||
// A connection that has completed version negotiation.
|
||||
/* A connection that has completed version negotiation. */
|
||||
pub struct MTPConnection {
|
||||
pub version: Version,
|
||||
pub codec: VersionedCodec,
|
||||
|
|
@ -47,7 +47,7 @@ pub struct MTPConnection {
|
|||
pub client_public_key: Option<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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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
107
type-map/build.rs
Normal file → Executable 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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ pub mod error;
|
|||
pub mod message;
|
||||
pub mod transport;
|
||||
|
||||
#[cfg(not(test))]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(not(test))]
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue