Docs & wasm
This commit is contained in:
parent
29427d301b
commit
be4b76dcd5
7 changed files with 953 additions and 6 deletions
325
NATIVE-CLIENT.md
Normal file
325
NATIVE-CLIENT.md
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
# MTP Native Client
|
||||||
|
|
||||||
|
The native client is a Rust library (`mtp-client`) for connecting to an MTP host over QUIC. It uses `wtransport` under the hood and provides both unauthenticated and authenticated (crypto handshake) connection modes.
|
||||||
|
|
||||||
|
## Cargo Dependency
|
||||||
|
|
||||||
|
Add the `mtp` umbrella crate with the `client` feature (and optionally `crypto` for authentication):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
mtp = { path = "/path/to/mtp", features = ["client"] }
|
||||||
|
|
||||||
|
# Add crypto for auth_connect / auth_register:
|
||||||
|
mtp = { path = "/path/to/mtp", features = ["client", "crypto"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## ClientConfig
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::client::ClientConfig;
|
||||||
|
|
||||||
|
let config = ClientConfig {
|
||||||
|
url: "https://host.example.com:4433".into(),
|
||||||
|
server_cert: None, // None = use system root certificates
|
||||||
|
client_id: 0, // previously assigned ID or 0
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|--------------|--------------------|-----------------------------------------------------|
|
||||||
|
| `url` | `String` | `https://host:port` address of the MTP host |
|
||||||
|
| `server_cert`| `Option<Vec<u8>>` | `None` to use system roots, `Some(pem_bytes)` to pin |
|
||||||
|
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
|
||||||
|
|
||||||
|
### TLS Certificate Handling
|
||||||
|
|
||||||
|
When `server_cert` is `None` (the default), the client loads the **system's
|
||||||
|
native root certificate store** via `rustls_native_certs`. This works with
|
||||||
|
publicly-trusted CAs out of the box on Linux (using `openssl-probe`), macOS
|
||||||
|
(Keychain), and Windows (Root Store).
|
||||||
|
|
||||||
|
For development or self-signed certificates, provide one or more PEM-encoded
|
||||||
|
certificates:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let pem = std::fs::read("my-server-cert.pem")?;
|
||||||
|
let config = ClientConfig {
|
||||||
|
server_cert: Some(pem),
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
When pinned, **only** the given certificate(s) are trusted for the TLS
|
||||||
|
handshake.
|
||||||
|
|
||||||
|
## Connection Methods
|
||||||
|
|
||||||
|
All methods return a `Result<MTPConnection, CommunicationError>`.
|
||||||
|
|
||||||
|
### MTPConnection
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MTPConnection {
|
||||||
|
pub version: Version,
|
||||||
|
pub sender: Sender,
|
||||||
|
pub receiver: Receiver,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub auth_state: AuthState,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub client_id: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `version` -- the negotiated protocol version
|
||||||
|
- `sender` / `receiver` -- for message I/O
|
||||||
|
- `client_id` -- the confirmed/assigned client identifier (crypto only)
|
||||||
|
|
||||||
|
### Unauthenticated Connect
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::client::{MTPClient, ClientConfig};
|
||||||
|
|
||||||
|
let config = ClientConfig {
|
||||||
|
url: "https://host.example.com:4433".into(),
|
||||||
|
server_cert: None,
|
||||||
|
client_id: 42,
|
||||||
|
};
|
||||||
|
|
||||||
|
let conn = MTPClient::connect(config).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Sends an `Identification` frame with the compiled-in protocol version and
|
||||||
|
client ID. No cryptographic handshake is performed.
|
||||||
|
|
||||||
|
### Authenticated Login
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::client::MTPClient;
|
||||||
|
use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||||
|
|
||||||
|
let keys = Keyring::from_bytes(&saved_keyring_bytes)?;
|
||||||
|
let host_pk = PublicKeyBundle::from_bytes(&saved_host_pk_bytes)?;
|
||||||
|
|
||||||
|
let config = ClientConfig {
|
||||||
|
client_id: 42, // must match the keyring's identity
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
6. Client verifies the host signature and nonce echo
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (ed_signer, sig_sk, sig_pk) = mtp::crypto::Ed25519Signer::generate();
|
||||||
|
let (pq_signer, sig_pq_sk, sig_pq_pk) = mtp::crypto::MlDsaSigner::generate();
|
||||||
|
let (kem_sk, kem_pk) = mtp::crypto::HybridKem::generate_keypair();
|
||||||
|
|
||||||
|
let keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);
|
||||||
|
|
||||||
|
let conn = MTPClient::auth_register(config, &keyring, &host_pk).await?;
|
||||||
|
|
||||||
|
// Save for next session
|
||||||
|
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
|
||||||
|
6. Client verifies the host signature and nonce echo
|
||||||
|
|
||||||
|
## Key Material
|
||||||
|
|
||||||
|
### Keyring
|
||||||
|
|
||||||
|
A `Keyring` bundles all secret and public key material for one identity:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Keyring {
|
||||||
|
pub kem_secret_key: KemPrivateKey,
|
||||||
|
pub kem_public_key: KemPublicKey,
|
||||||
|
pub sig_cl_secret_key: SignaturePrivateKey, // Ed25519
|
||||||
|
pub sig_cl_public_key: SignaturePublicKey, // Ed25519
|
||||||
|
pub sig_pq_secret_key: SignaturePqPrivateKey, // ML-DSA-65
|
||||||
|
pub sig_pq_public_key: SignaturePqPublicKey, // ML-DSA-65
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Serialise: `keyring.to_bytes()` -> `Vec<u8>`
|
||||||
|
- Deserialise: `Keyring::from_bytes(&bytes)` -> `Result<Keyring, CryptoError>`
|
||||||
|
- Get public half: `keyring.public_key_bundle()` -> `PublicKeyBundle`
|
||||||
|
|
||||||
|
### PublicKeyBundle
|
||||||
|
|
||||||
|
The public half of a keyring, used by the host for signature verification and
|
||||||
|
by the client for host signature verification:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct PublicKeyBundle {
|
||||||
|
pub kem_public_key: KemPublicKey,
|
||||||
|
pub sig_cl_public_key: SignaturePublicKey,
|
||||||
|
pub sig_pq_public_key: SignaturePqPublicKey,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Obtain the host's `PublicKeyBundle` out of band (e.g. from files exported by
|
||||||
|
the host, or from a trusted directory).
|
||||||
|
|
||||||
|
## Sending and Receiving Messages
|
||||||
|
|
||||||
|
### CommunicationValue
|
||||||
|
|
||||||
|
Messages are `CommunicationValue` frames. Construct them with the builder API:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::codec::{CommunicationValue, CommunicationType, DataType, DataValue};
|
||||||
|
use mtp::type_map::TypeMap;
|
||||||
|
|
||||||
|
let msg = CommunicationValue::new(CommunicationType::Ping)
|
||||||
|
.with_sender(conn.client_id)
|
||||||
|
.add_typed_default(DataType::Description, DataValue::Str("hello".into()))
|
||||||
|
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(now))
|
||||||
|
.to_bytes();
|
||||||
|
```
|
||||||
|
|
||||||
|
When the `registry` feature is enabled (via the `host` feature), you can also
|
||||||
|
use `add_typed` with a `TypeMap` to resolve data type names from your project's
|
||||||
|
type-map configuration.
|
||||||
|
|
||||||
|
### Send
|
||||||
|
|
||||||
|
```rust
|
||||||
|
conn.sender.send(&msg).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Two send modes (configured via `mtp::transport::Policy`):
|
||||||
|
- `PersistentStream` (default) -- reuses one QUIC uni-directional stream
|
||||||
|
- `SingleStreamPerMessage` -- opens a new stream per message
|
||||||
|
|
||||||
|
### Receive
|
||||||
|
|
||||||
|
```rust
|
||||||
|
match conn.receiver.receive().await {
|
||||||
|
Ok(msg) => { /* handle CommunicationValue */ }
|
||||||
|
Err(e) => { /* connection closed or error */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Inbound frames are queued internally. The `receive()` method returns the next
|
||||||
|
available message.
|
||||||
|
|
||||||
|
### Close
|
||||||
|
|
||||||
|
```rust
|
||||||
|
conn.sender.close();
|
||||||
|
// or
|
||||||
|
conn.receiver.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
Sends a close frame and signals the peer. The `Sender::close()` spawns an async
|
||||||
|
task that sends the frame, waits for `force_close_delay` (default 300ms), then
|
||||||
|
force-closes the QUIC connection if the peer has not already done so.
|
||||||
|
|
||||||
|
## Crypto Containers
|
||||||
|
|
||||||
|
With the `crypto` feature, `DataValue` supports encrypted, signed, and
|
||||||
|
signed+encrypted containers:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::crypto::{ChaCha20Poly1305, Ed25519Signer, SigAlgorithm};
|
||||||
|
|
||||||
|
let cipher = ChaCha20Poly1305::new(derive_encryption_key(...));
|
||||||
|
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)?;
|
||||||
|
|
||||||
|
// Encrypted container
|
||||||
|
let mut enc = DataValue::Container(vec![
|
||||||
|
(DataTypeId(1), DataValue::Str("secret".into())),
|
||||||
|
]);
|
||||||
|
enc.encrypt_container(&cipher, b"aad");
|
||||||
|
|
||||||
|
// Signed container
|
||||||
|
let mut sig = DataValue::Container(vec![
|
||||||
|
(DataTypeId(1), DataValue::Str("signed".into())),
|
||||||
|
]);
|
||||||
|
sig.sign_container(SigAlgorithm::ED25519, &signer);
|
||||||
|
|
||||||
|
// Signed + encrypted
|
||||||
|
let mut sec = DataValue::Container(vec![
|
||||||
|
(DataTypeId(1), DataValue::Str("both".into())),
|
||||||
|
]);
|
||||||
|
sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"aad");
|
||||||
|
```
|
||||||
|
|
||||||
|
On the receiving side, use the corresponding `decrypt_into_container`,
|
||||||
|
`verify_into_container`, or `decrypt_signed_encrypted_container` methods.
|
||||||
|
|
||||||
|
## Policy Configuration
|
||||||
|
|
||||||
|
The `Policy` struct controls transport behaviour:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::transport::{Policy, SendMode};
|
||||||
|
|
||||||
|
let policy = Policy {
|
||||||
|
send_mode: SendMode::PersistentStream,
|
||||||
|
max_message_size: 1_000_000_000,
|
||||||
|
open_stream_timeout: Duration::from_millis(2000),
|
||||||
|
write_timeout: Duration::from_millis(2000),
|
||||||
|
read_timeout: Duration::from_millis(30_000),
|
||||||
|
keep_alive_interval: Some(Duration::from_secs(3)),
|
||||||
|
max_idle_timeout: Some(Duration::from_secs(30)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
To apply a custom policy, call `mtp_transport::connect()` directly instead of
|
||||||
|
using `MTPClient`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp_transport::{connect, Policy};
|
||||||
|
|
||||||
|
let (sender, receiver) = connect(&config.url, config.server_cert, policy).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build and send the initial `Identification` frame manually to complete
|
||||||
|
version negotiation.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
The client's protocol version is baked in at compile time via the
|
||||||
|
`PROTOCOL_VERSION` constant from `mtp_codec`. The version is set by the
|
||||||
|
`protocol_version` field in your `type-maps.yaml`.
|
||||||
|
|
||||||
|
The client never imports the `registry` module; it uses a single compiled-in
|
||||||
|
version and expects the host to negotiate a compatible version.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
`CommunicationError` covers transport errors:
|
||||||
|
|
||||||
|
| Variant | Meaning |
|
||||||
|
|-------------------------|--------------------------------------------|
|
||||||
|
| `StreamClosed` | Connection was closed by peer or timed out |
|
||||||
|
| `StreamError` | Transport-level I/O error |
|
||||||
|
| `MessageTooLarge` | Frame exceeds `max_message_size` |
|
||||||
|
| `ParseCommunicationValue` | Failed to deserialize incoming frame |
|
||||||
|
| `AuthenticationFailed` | Nonce mismatch or invalid host signature |
|
||||||
|
| `ConnectionError` | QUIC connection failure |
|
||||||
|
| `UseAfterClosed` | Attempted send/receive after close |
|
||||||
|
|
||||||
|
|
||||||
317
NATIVE-HOST.md
Normal file
317
NATIVE-HOST.md
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
# MTP Native Host
|
||||||
|
|
||||||
|
The native host is a Rust library (`mtp-host`) that runs a QUIC server, accepts
|
||||||
|
MTP client connections, negotiates protocol versions, and optionally performs a
|
||||||
|
mutual-authentication handshake (login/register) using Ed25519 and ML-DSA-65
|
||||||
|
signatures.
|
||||||
|
|
||||||
|
## Cargo Dependency
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
mtp = { path = "/path/to/mtp", features = ["host"] }
|
||||||
|
|
||||||
|
# Add crypto for authenticated connections:
|
||||||
|
mtp = { path = "/path/to/mtp", features = ["host", "crypto"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## HostConfig
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::host::HostConfig;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
|
let config = HostConfig {
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||||
|
port: 4433,
|
||||||
|
tls_fullchain: std::fs::read("cert.pem")?,
|
||||||
|
tls_key: std::fs::read("key.pem")?,
|
||||||
|
|
||||||
|
// Crypto fields (required when feature = "crypto"):
|
||||||
|
require_authentication: true,
|
||||||
|
host_id: 1,
|
||||||
|
host_keyring: /* Keyring */,
|
||||||
|
get_existing_user: Box::new(|client_id: u64| -> Option<PublicKeyBundle> {
|
||||||
|
CLIENT_DB.lock().unwrap().get(&client_id).cloned()
|
||||||
|
}),
|
||||||
|
complete_register: Box::new(|bundle: PublicKeyBundle| -> u64 {
|
||||||
|
let id = next_id();
|
||||||
|
CLIENT_DB.lock().unwrap().insert(id, bundle);
|
||||||
|
id
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|------------------------|-----------------------------------------|------------------------------------------------|
|
||||||
|
| `ip` | `IpAddr` | Bind address |
|
||||||
|
| `port` | `u16` | Listen port |
|
||||||
|
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
|
||||||
|
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key |
|
||||||
|
| `require_authentication` | `bool` (crypto) | Enable login/register handshake |
|
||||||
|
| `host_id` | `u64` (crypto) | Host identifier |
|
||||||
|
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
|
||||||
|
| `get_existing_user` | `Box<dyn Fn(u64) -> Option<PublicKeyBundle> + Send>` (crypto) | Lookup callback for login |
|
||||||
|
| `complete_register` | `Box<dyn Fn(PublicKeyBundle) -> u64 + Send>` (crypto) | Registration callback, returns new client ID |
|
||||||
|
|
||||||
|
### TLS
|
||||||
|
|
||||||
|
The host requires a TLS certificate. For development, generate a self-signed
|
||||||
|
certificate using `rcgen`. For production, use a CA-signed certificate.
|
||||||
|
|
||||||
|
## Accepting Connections
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::host::MTPHost;
|
||||||
|
|
||||||
|
let mut host = MTPHost::new(config).await?;
|
||||||
|
println!("Listening on {}", host.local_addr());
|
||||||
|
|
||||||
|
while let Some(conn) = host.accept().await {
|
||||||
|
// conn is an MTPConnection ready for I/O
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MTPConnection
|
||||||
|
|
||||||
|
Returned by `accept()` after version negotiation (and authentication if
|
||||||
|
enabled):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct MTPConnection {
|
||||||
|
pub version: Version,
|
||||||
|
pub codec: VersionedCodec,
|
||||||
|
pub sender: Sender,
|
||||||
|
pub receiver: Receiver,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub auth_state: AuthState,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub client_id: u64,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub client_public_key: Option<PublicKeyBundle>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `version` -- the negotiated protocol version
|
||||||
|
- `codec` -- a `VersionedCodec` scoped to the negotiated version (use for
|
||||||
|
version-aware encode/decode)
|
||||||
|
- `sender` / `receiver` -- for message I/O
|
||||||
|
- `client_id` -- the authenticated client's ID
|
||||||
|
- `client_public_key` -- the client's public key bundle (for signature
|
||||||
|
verification of subsequent messages)
|
||||||
|
|
||||||
|
## Version Negotiation
|
||||||
|
|
||||||
|
When a client connects, `accept()` performs the following sequence:
|
||||||
|
|
||||||
|
1. Accept the QUIC connection
|
||||||
|
2. Read the client's first `CommunicationValue` (always encoded with reserved
|
||||||
|
type IDs)
|
||||||
|
3. Extract the protocol version from `DataType::Version` (wire ID 3) as a
|
||||||
|
`DataValue::Str("major.minor")`
|
||||||
|
4. Call `registry.negotiate(&[client_version])` to find the highest mutually
|
||||||
|
supported version
|
||||||
|
5. Return `None` (closing the connection) if no compatible version exists
|
||||||
|
6. Return an `MTPConnection` with the negotiated version
|
||||||
|
|
||||||
|
The `Registry` is built automatically from all type maps defined in your
|
||||||
|
`type-maps.yaml` via `Registry::builtin()`.
|
||||||
|
|
||||||
|
### Registry
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::codec::registry::Registry;
|
||||||
|
|
||||||
|
let registry = host.registry();
|
||||||
|
assert!(registry.supports(&Version(2, 0)));
|
||||||
|
|
||||||
|
let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
|
||||||
|
// -> Some(Version(2, 0)) if both versions are registered
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Flow (crypto feature)
|
||||||
|
|
||||||
|
When `require_authentication` is `true`, `accept()` runs an authenticated
|
||||||
|
handshake before returning the connection. The flow is:
|
||||||
|
|
||||||
|
### Login (existing client)
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Host
|
||||||
|
| |
|
||||||
|
| QUIC connect |
|
||||||
|
|---------------------------------------->|
|
||||||
|
| |
|
||||||
|
| Identification { |
|
||||||
|
| Version, Id, ClientNonce, |
|
||||||
|
| Signature, [PqSignature] |
|
||||||
|
| } |
|
||||||
|
|---------------------------------------->|
|
||||||
|
| | lookup get_existing_user(client_id)
|
||||||
|
| | verify Ed25519 (and optional ML-DSA) sig
|
||||||
|
| IdentificationResponse { |
|
||||||
|
| Connected=true, ClientNonce(echoed), |
|
||||||
|
| Id, Timestamp(new_nonce), |
|
||||||
|
| Signature, [PqSignature] |
|
||||||
|
| } |
|
||||||
|
|<----------------------------------------|
|
||||||
|
```
|
||||||
|
|
||||||
|
The client signature payload is: `version_string || client_id (8 bytes, big-endian) || client_nonce (16 bytes, big-endian)`
|
||||||
|
|
||||||
|
The host signs: `0x01 || assigned_id (8 bytes, big-endian) || client_nonce (16 bytes) || host_new_nonce (16 bytes)`
|
||||||
|
|
||||||
|
### Register (new client)
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Host
|
||||||
|
| |
|
||||||
|
| QUIC connect |
|
||||||
|
|---------------------------------------->|
|
||||||
|
| |
|
||||||
|
| Register { |
|
||||||
|
| Version, ClientNonce, |
|
||||||
|
| PublicKeys (serialized PublicKeyBundle),
|
||||||
|
| Signature, [PqSignature] |
|
||||||
|
| } |
|
||||||
|
|---------------------------------------->|
|
||||||
|
| | extract PublicKeyBundle from frame
|
||||||
|
| | verify Ed25519 (and optional ML-DSA) sig
|
||||||
|
| | call complete_register(bundle) -> new_id
|
||||||
|
| RegisterResponse { |
|
||||||
|
| Connected=true, ClientNonce(echoed), |
|
||||||
|
| Id, Timestamp(new_nonce), |
|
||||||
|
| Signature, [PqSignature] |
|
||||||
|
| } |
|
||||||
|
|<----------------------------------------|
|
||||||
|
```
|
||||||
|
|
||||||
|
The client signature payload is: `version_string || client_nonce (16 bytes) || public_key_bytes`
|
||||||
|
|
||||||
|
After a successful handshake, `accept()` returns an `MTPConnection` with
|
||||||
|
`auth_state = Authenticated`, `client_id` set, and `client_public_key`
|
||||||
|
available for verifying subsequent signed messages from the client.
|
||||||
|
|
||||||
|
### Rejection
|
||||||
|
|
||||||
|
If verification fails or the client is not found (login), the host sends a
|
||||||
|
rejection response with `Connected=false` and closes the send stream, returning
|
||||||
|
`None` from `accept()`.
|
||||||
|
|
||||||
|
## Handling Messages
|
||||||
|
|
||||||
|
Use `conn.sender` and `conn.receiver` for bidirectional message exchange:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
while let Some(conn) = host.accept().await {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match conn.receiver.receive().await {
|
||||||
|
Ok(msg) => {
|
||||||
|
let response = process_message(&msg, &conn);
|
||||||
|
conn.sender.send(&response).await.ok();
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Versioned Codec
|
||||||
|
|
||||||
|
The `conn.codec` is a `VersionedCodec` pre-configured with the negotiated
|
||||||
|
version. Use it to encode/decode with version-specific type maps:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let tm = conn.codec.registry().get(&conn.version).unwrap();
|
||||||
|
|
||||||
|
// Look up type IDs for the negotiated version
|
||||||
|
let desc_id = DataTypeId(tm.data_id_enum(DataType::Description).unwrap());
|
||||||
|
let value = msg.get_data(desc_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Host Callbacks
|
||||||
|
|
||||||
|
### get_existing_user
|
||||||
|
|
||||||
|
Called during login to retrieve a client's public key bundle for signature
|
||||||
|
verification. Must return `Some(PublicKeyBundle)` if the client ID is known,
|
||||||
|
or `None` to reject.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let get_existing_user = Box::new(|id: u64| -> Option<PublicKeyBundle> {
|
||||||
|
db.lock().unwrap().get(&id).cloned()
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### complete_register
|
||||||
|
|
||||||
|
Called during registration to persist a new client's public key bundle and
|
||||||
|
assign a client ID. The returned `u64` becomes the client's permanent
|
||||||
|
identifier.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let complete_register = Box::new(|bundle: PublicKeyBundle| -> u64 {
|
||||||
|
let mut db = db.lock().unwrap();
|
||||||
|
let id = next_id;
|
||||||
|
next_id += 1;
|
||||||
|
db.insert(id, bundle);
|
||||||
|
id
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Both callbacks are called from within `accept()` and must be `Send`. They are
|
||||||
|
invoked synchronously, so avoid long-running operations (or use `spawn_blocking`
|
||||||
|
if needed, though the callbacks are `Fn`, not `AsyncFn`).
|
||||||
|
|
||||||
|
## Host Key Generation
|
||||||
|
|
||||||
|
Generate a host keyring once and persist it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner};
|
||||||
|
use mtp::crypto::kem::HybridKem;
|
||||||
|
|
||||||
|
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||||
|
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||||
|
let (kem_sk, kem_pk) = HybridKem::generate_keypair();
|
||||||
|
|
||||||
|
let host_keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
let bytes = host_keyring.to_bytes();
|
||||||
|
std::fs::write("host_keys.bin", bytes)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Export the public key bundle so clients can verify the host identity:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let bundle = host_keyring.public_key_bundle();
|
||||||
|
std::fs::write("host_enc_kem_pk.bin", bundle.kem_public_key.as_bytes())?;
|
||||||
|
std::fs::write("host_sig_pk.bin", bundle.sig_cl_public_key.as_bytes())?;
|
||||||
|
std::fs::write("host_sig_pq_pk.bin", bundle.sig_pq_public_key.as_bytes())?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Policy
|
||||||
|
|
||||||
|
The transport `Policy` is set to defaults internally. To customise (timeouts,
|
||||||
|
send mode, etc.), use `mtp_transport::host()` directly instead of `MTPHost`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp_transport::{host, Policy};
|
||||||
|
|
||||||
|
let transport = host(ip, port, cert, key, custom_policy).await?;
|
||||||
|
// Then build version negotiation on top:
|
||||||
|
// - accept transport.next()
|
||||||
|
// - read first frame
|
||||||
|
// - registry.negotiate()
|
||||||
|
// - return MTPConnection
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
Drop the `MTPHost` to stop accepting new connections. Active connections
|
||||||
|
continue until their `Sender`/`Receiver` are dropped or the peer disconnects.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
MTP is a modular transport protocol built on QUIC. It provides version-negotiable type maps, a binary codec, cryptographic primitives (classical and post-quantum), and host/client connection management with mutual authentication.
|
MTP is a modular transport protocol built on QUIC. It provides version-negotiable type maps, a binary codec, cryptographic primitives (classical and post-quantum), and host/client connection management with mutual authentication.
|
||||||
|
|
||||||
|
There are Area specific docs when working with seperate concerns for the [Native-Client](./NATIVE-CLIENT.md), [WASM-Client](./WASM-CLIENT.md) & [Host](./NATIVE-HOST.md)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Add the `mtp` crate with your desired features:
|
Add the `mtp` crate with your desired features:
|
||||||
|
|
|
||||||
281
WASM-CLIENT.md
Normal file
281
WASM-CLIENT.md
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
# MTP WASM Client
|
||||||
|
|
||||||
|
The WASM client is a browser-compatible MTP implementation that uses the **WebTransport** API to communicate with an MTP host over QUIC (HTTP/3). It compiles from Rust to WebAssembly via `wasm-bindgen` and exposes a JavaScript/TypeScript API through the `mtp-wasm` npm package.
|
||||||
|
|
||||||
|
## Package
|
||||||
|
|
||||||
|
The compiled package lives in `wasm/pkg/` and contains:
|
||||||
|
|
||||||
|
- `mtp_wasm.js` -- generated JS glue
|
||||||
|
- `mtp_wasm_bg.wasm` -- the WebAssembly binary
|
||||||
|
- `mtp_wasm.d.ts` -- TypeScript type declarations
|
||||||
|
- `package.json` -- npm package definition
|
||||||
|
|
||||||
|
Install or copy these files into your web project. Then initialise the module:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import init, { WasmClient } from 'mtp-wasm';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
WebTransport is required. Check availability at runtime:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (!WasmClient.is_supported()) {
|
||||||
|
// fall back or show an error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connecting to a Host
|
||||||
|
|
||||||
|
### ConnectionConfig
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config = new ConnectionConfig("https://host.example.com:4433");
|
||||||
|
config.client_id = 12345n; // optional, for re-authentication
|
||||||
|
config.server_certificate_hashes = [ // optional, for certificate pinning
|
||||||
|
"sha256:abc123...",
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
`client_id` is only needed for authenticated login (`auth_connect`). For registration (`auth_register`) it is ignored.
|
||||||
|
|
||||||
|
### TLS Certificate Handling
|
||||||
|
|
||||||
|
By default — when `server_certificate_hashes` is not set — the browser uses its
|
||||||
|
**built-in root certificate store** to verify the server's TLS certificate,
|
||||||
|
just like any other HTTPS/WebSocket connection. This works with publicly-trusted
|
||||||
|
certificate authorities automatically.
|
||||||
|
|
||||||
|
For development or self-signed certificates, pin the server certificate by
|
||||||
|
providing its hash:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
config.server_certificate_hashes = [
|
||||||
|
"sha256:abcd1234...", // hex-encoded hash value
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
The hash format is `"<algorithm>:<hex-encoded-hash>"`. When hashes are
|
||||||
|
provided, the browser **only** trusts certificates matching one of the given
|
||||||
|
hashes and ignores its root store for this connection.
|
||||||
|
|
||||||
|
### Callbacks
|
||||||
|
|
||||||
|
The client uses three callbacks for state, messages, and errors:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const client = new WasmClient(
|
||||||
|
(state: number) => console.log("state", state), // ConnectionState enum
|
||||||
|
(data: Uint8Array) => console.log("msg", data), // raw frame bytes
|
||||||
|
(err: any) => console.error("err", err), // error description
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection States
|
||||||
|
|
||||||
|
| Value | Name |
|
||||||
|
|-------|--------------|
|
||||||
|
| 0 | Disconnected |
|
||||||
|
| 1 | Connecting |
|
||||||
|
| 2 | Connected |
|
||||||
|
| 3 | Failed |
|
||||||
|
|
||||||
|
Poll `client.state` at any time.
|
||||||
|
|
||||||
|
## Connection Methods
|
||||||
|
|
||||||
|
### Unauthenticated Connect
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const confirmedId = await client.auth_connect(
|
||||||
|
config,
|
||||||
|
hostPublicKeyBytes, // Uint8Array: serialized PublicKeyBundle from the host
|
||||||
|
keyringBytes, // Uint8Array: serialized Keyring matching the client ID
|
||||||
|
clientId, // bigint: previously assigned client ID
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Exchange: client sends a signed `Identification` frame, the host verifies it and
|
||||||
|
responds with a signed `IdentificationResponse`. Returns the confirmed client ID.
|
||||||
|
|
||||||
|
### Registration (new client)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const newId = await client.auth_register(
|
||||||
|
config,
|
||||||
|
hostPublicKeyBytes, // Uint8Array: serialized PublicKeyBundle from the host
|
||||||
|
keyringBytes, // Uint8Array: serialized Keyring for the new identity
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Exchange: client sends a signed `Register` frame with public keys, the host
|
||||||
|
assigns a new ID and responds with a signed `RegisterResponse`. Returns the
|
||||||
|
newly assigned client ID.
|
||||||
|
|
||||||
|
## Sending and Receiving Messages
|
||||||
|
|
||||||
|
### Send
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const frame = build_ping_frame(clientId, "hello", timestamp, data);
|
||||||
|
await client.send(frame);
|
||||||
|
```
|
||||||
|
|
||||||
|
`send()` takes raw frame bytes (a serialized `CommunicationValue`). Build frames
|
||||||
|
with the provided helper functions or construct them manually.
|
||||||
|
|
||||||
|
### Receive
|
||||||
|
|
||||||
|
Incoming frames arrive on the `on_message` callback registered in the constructor.
|
||||||
|
The callback receives a `Uint8Array` of raw frame bytes. Parse with
|
||||||
|
`CommunicationValue.from_bytes()` on the Rust side or handle the bytes in JS.
|
||||||
|
|
||||||
|
### Disconnect
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
client.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
Gracefully closes the WebTransport session.
|
||||||
|
|
||||||
|
## Building Frames
|
||||||
|
|
||||||
|
### `build_ping_frame`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function build_ping_frame(
|
||||||
|
clientId: bigint,
|
||||||
|
description: string,
|
||||||
|
timestamp: bigint,
|
||||||
|
data: Uint8Array,
|
||||||
|
): Uint8Array;
|
||||||
|
```
|
||||||
|
|
||||||
|
Constructs a basic `Ping` message with description, timestamp, and optional
|
||||||
|
binary payload. Useful for health checks and simple messaging.
|
||||||
|
|
||||||
|
### `build_demo_message`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function build_demo_message(
|
||||||
|
clientId: bigint,
|
||||||
|
keyringBytes: Uint8Array,
|
||||||
|
): Uint8Array;
|
||||||
|
```
|
||||||
|
|
||||||
|
Constructs a `Ping` frame that demonstrates encrypted, signed, and
|
||||||
|
signed+encrypted containers using a deterministic demo key. The paired host
|
||||||
|
handler can decrypt and verify these containers if it knows the same shared
|
||||||
|
secret.
|
||||||
|
|
||||||
|
### `parse_auth_response`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function parse_auth_response(response: Uint8Array): any;
|
||||||
|
```
|
||||||
|
|
||||||
|
Parses an `IdentificationResponse` or `RegisterResponse` frame into a JS object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
connected: boolean,
|
||||||
|
clientNonce?: Uint8Array,
|
||||||
|
assignedId?: number,
|
||||||
|
timestamp?: number,
|
||||||
|
signature?: Uint8Array,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Crypto Primitives
|
||||||
|
|
||||||
|
### Key Generation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = ed25519_generate();
|
||||||
|
// result.signer -> WasmEd25519Signer
|
||||||
|
// result.secretKey -> Uint8Array (32 bytes)
|
||||||
|
// result.publicKey -> Uint8Array (32 bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyring
|
||||||
|
|
||||||
|
A `Keyring` bundles all key material for an identity. For Ed25519-only setups:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const keyringBytes = keyring_from_ed25519(secretKey, publicKey);
|
||||||
|
// keyringBytes is ready for WasmClient.auth_register or WasmClient.auth_connect
|
||||||
|
```
|
||||||
|
|
||||||
|
Full keyring with KEM + ML-DSA requires constructing on the Rust side. The
|
||||||
|
serialized bytes are portable:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const keyring = WasmKeyring.from_bytes(keyringBytes);
|
||||||
|
const bundle = keyring.public_key_bundle();
|
||||||
|
// bundle.kem_public_key -> Uint8Array
|
||||||
|
// bundle.sig_cl_public_key -> Uint8Array
|
||||||
|
// bundle.sig_pq_public_key -> Uint8Array
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signing and Verification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const signer = new WasmEd25519Signer(secretKey);
|
||||||
|
const sig = signer.sign(message); // Uint8Array
|
||||||
|
signer.verify(message, sig); // throws on mismatch
|
||||||
|
|
||||||
|
// Standalone verification (no signer object needed):
|
||||||
|
ed25519_verify(publicKey, message, signature);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Symmetric Encryption
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cipher = new WasmChaCha20Poly1305(key); // 32-byte key
|
||||||
|
const encrypted = cipher.encrypt(plaintext, aad); // nonce || ciphertext
|
||||||
|
const decrypted = cipher.decrypt(encrypted, aad);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hashing and KDF
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const hash = wasm_sha256(data); // 32 bytes
|
||||||
|
const double = wasm_sha256_double(data); // SHA-256(SHA-256(data))
|
||||||
|
|
||||||
|
const derived = wasm_hkdf_expand(ikm, salt, info, len);
|
||||||
|
const encKey = wasm_derive_encryption_key(ikm, salt, context); // 32 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle and Best Practices
|
||||||
|
|
||||||
|
1. **Key persistence** -- serialise keyring bytes after registration and store
|
||||||
|
them (e.g. in `localStorage`). On next visit, load the saved keyring and
|
||||||
|
call `auth_connect` instead of registering again.
|
||||||
|
|
||||||
|
2. **Ownership** -- call `config.free()` after connecting if the config object
|
||||||
|
is no longer needed. WASM objects (`WasmClient`, `WasmKeyring`, etc.) are
|
||||||
|
garbage-collected, but explicit `free()` or `dispose()` reclaims memory
|
||||||
|
sooner.
|
||||||
|
|
||||||
|
3. **Receive loop** -- once `connect`, `auth_connect`, or `auth_register`
|
||||||
|
resolves, the receive loop is running in the background. Incoming frames
|
||||||
|
arrive on the `on_message` callback. There is no need to poll.
|
||||||
|
|
||||||
|
4. **Single active client** -- a `WasmClient` manages one WebTransport session.
|
||||||
|
Create a new instance for each connection.
|
||||||
|
|
||||||
|
5. **State transitions** -- after `disconnect()` the client transitions to
|
||||||
|
`Disconnected`. The instance is reusable; call a connect method again to
|
||||||
|
open a new session.
|
||||||
|
|
@ -28,6 +28,8 @@ web-sys = { version = "0.3", features = [
|
||||||
] }
|
] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde-wasm-bindgen = "0.6"
|
serde-wasm-bindgen = "0.6"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ impl WasmClient {
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn connect(&mut self, config: &ConnectionConfig) -> Result<(), JsValue> {
|
pub async fn connect(&mut self, config: &ConnectionConfig) -> Result<(), JsValue> {
|
||||||
self.set_state(ConnectionState::Connecting);
|
self.set_state(ConnectionState::Connecting);
|
||||||
let transport = WasmTransport::connect(&config.url).await?;
|
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||||
let inner = transport.inner().clone();
|
let inner = transport.inner().clone();
|
||||||
|
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
|
|
@ -157,7 +157,7 @@ impl WasmClient {
|
||||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||||
.to_bytes();
|
.to_bytes();
|
||||||
|
|
||||||
let transport = WasmTransport::connect(&config.url).await?;
|
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||||
let inner = transport.inner().clone();
|
let inner = transport.inner().clone();
|
||||||
transport.send_frame(&frame).await?;
|
transport.send_frame(&frame).await?;
|
||||||
|
|
||||||
|
|
@ -249,7 +249,7 @@ impl WasmClient {
|
||||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||||
.to_bytes();
|
.to_bytes();
|
||||||
|
|
||||||
let transport = WasmTransport::connect(&config.url).await?;
|
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||||
let inner = transport.inner().clone();
|
let inner = transport.inner().clone();
|
||||||
transport.send_frame(&frame).await?;
|
transport.send_frame(&frame).await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::WebTransport;
|
use web_sys::{WebTransport, WebTransportHash, WebTransportOptions};
|
||||||
|
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
|
|
||||||
|
|
@ -11,8 +11,28 @@ pub struct WasmTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WasmTransport {
|
impl WasmTransport {
|
||||||
pub async fn connect(url: &str) -> Result<Self, JsValue> {
|
pub async fn connect(url: &str, cert_hashes: Option<Vec<String>>) -> Result<Self, JsValue> {
|
||||||
let transport = WebTransport::new(url)?;
|
let transport = match cert_hashes {
|
||||||
|
Some(hashes) => {
|
||||||
|
let opts = WebTransportOptions::new();
|
||||||
|
let mut wt_hashes = Vec::new();
|
||||||
|
for h in hashes {
|
||||||
|
if let Some((algo, hex_val)) = h.split_once(':') {
|
||||||
|
if let Ok(bytes) = hex::decode(hex_val) {
|
||||||
|
let hash = WebTransportHash::new();
|
||||||
|
hash.set_algorithm(algo);
|
||||||
|
hash.set_value_u8_array(&js_sys::Uint8Array::from(&bytes[..]));
|
||||||
|
wt_hashes.push(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wt_hashes.is_empty() {
|
||||||
|
opts.set_server_certificate_hashes(&wt_hashes);
|
||||||
|
}
|
||||||
|
WebTransport::new_with_options(url, &opts)?
|
||||||
|
}
|
||||||
|
None => WebTransport::new(url)?,
|
||||||
|
};
|
||||||
JsFuture::from(transport.ready()).await
|
JsFuture::from(transport.ready()).await
|
||||||
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
|
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
|
||||||
Ok(Self { inner: transport })
|
Ok(Self { inner: transport })
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue