Merge remote-tracking branch 'origin/master'
Some checks failed
CI / checks (push) Failing after 1m51s
Some checks failed
CI / checks (push) Failing after 1m51s
This commit is contained in:
commit
1b60ccfe43
9 changed files with 385 additions and 107 deletions
|
|
@ -22,6 +22,7 @@ pub struct ClientConfig {
|
|||
pub url: String,
|
||||
pub tls: ClientTlsConfig,
|
||||
pub client_id: u64,
|
||||
pub description: Option<String>,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_timeout: Duration,
|
||||
}
|
||||
|
|
@ -38,6 +39,7 @@ impl ClientConfig {
|
|||
url: url.into(),
|
||||
tls: ClientTlsConfig::SystemRoots,
|
||||
client_id: 0,
|
||||
description: None,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_timeout: Duration::from_secs(30),
|
||||
}
|
||||
|
|
@ -57,6 +59,11 @@ impl ClientConfig {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.auth_timeout = timeout;
|
||||
|
|
@ -76,6 +83,7 @@ pub struct MTPConnection {
|
|||
pub version: Version,
|
||||
pub sender: Sender,
|
||||
pub receiver: Receiver,
|
||||
pub description: Option<String>,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_state: AuthState,
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -150,12 +158,15 @@ impl MTPClient {
|
|||
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
||||
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
||||
let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
.add_typed_default(
|
||||
DataType::Id,
|
||||
DataValue::UnsignedNumber(config.client_id.into()),
|
||||
);
|
||||
if let Some(desc) = &config.description {
|
||||
ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||
}
|
||||
|
||||
sender.send(&ident).await?;
|
||||
|
||||
|
|
@ -163,6 +174,7 @@ impl MTPClient {
|
|||
version: PROTOCOL_VERSION,
|
||||
sender,
|
||||
receiver,
|
||||
description: config.description,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_state: AuthState::Unauthenticated,
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -379,12 +391,15 @@ impl MTPClient {
|
|||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
|
||||
// 1. Send the unsigned Identification hello (version + claimed id).
|
||||
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
||||
let mut 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),
|
||||
);
|
||||
if let Some(desc) = &config.description {
|
||||
ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||
}
|
||||
if let Err(e) = sender.send(&ident).await {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
|
|
@ -464,6 +479,7 @@ impl MTPClient {
|
|||
version: PROTOCOL_VERSION,
|
||||
sender,
|
||||
receiver,
|
||||
description: config.description,
|
||||
auth_state: AuthState::Authenticated,
|
||||
client_id: config.client_id,
|
||||
})
|
||||
|
|
@ -519,9 +535,12 @@ impl MTPClient {
|
|||
let pk_bytes = pk_bundle.as_bytes();
|
||||
|
||||
// 1. Send the unsigned Register hello (version + public-key bundle).
|
||||
let register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
|
||||
let mut 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()));
|
||||
if let Some(desc) = &config.description {
|
||||
register = register.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||
}
|
||||
if let Err(e) = sender.send(®ister).await {
|
||||
sender.close();
|
||||
return Err(e);
|
||||
|
|
@ -607,6 +626,7 @@ impl MTPClient {
|
|||
version: PROTOCOL_VERSION,
|
||||
sender,
|
||||
receiver,
|
||||
description: config.description,
|
||||
auth_state: AuthState::Authenticated,
|
||||
client_id: assigned_id,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ The host's `accept()` method:
|
|||
|
||||
### Login/Register Handshake
|
||||
|
||||
When `require_authentication` is set, the parties run a mutually-authenticated
|
||||
When `authentication_policy` is `ForceAuthentication` or `AllowAuthentication`,
|
||||
the parties run a mutually-authenticated
|
||||
**challenge-response**. The client speaks first with an *unsigned* hello:
|
||||
|
||||
- **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ let config = ClientConfig::new("https://host.example.com:4433")
|
|||
| `url` | `String` | `https://host:port` address of the MTP host |
|
||||
| `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` |
|
||||
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
|
||||
| `description` | `Option<String>` | Optional label sent during handshake (e.g. `"phone"`) |
|
||||
| `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) |
|
||||
|
||||
### TLS Certificate Handling
|
||||
|
|
@ -60,6 +61,7 @@ pub struct MTPConnection {
|
|||
pub version: Version,
|
||||
pub sender: Sender,
|
||||
pub receiver: Receiver,
|
||||
pub description: Option<String>,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_state: AuthState,
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -69,6 +71,7 @@ pub struct MTPConnection {
|
|||
|
||||
- `version` -- the negotiated protocol version
|
||||
- `sender` / `receiver` -- for message I/O
|
||||
- `description` -- the label sent during handshake (set via `ClientConfig::with_description`)
|
||||
- `client_id` -- the confirmed/assigned client identifier (crypto only)
|
||||
|
||||
### Unauthenticated Connect
|
||||
|
|
|
|||
|
|
@ -48,11 +48,30 @@ let config = HostConfig::new(
|
|||
| `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 |
|
||||
| `authentication_policy` | `AuthenticationPolicy` (crypto) | `ForceAuthentication`, `AllowAuthentication`, or `Unauthenticated` |
|
||||
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
|
||||
| `get_existing_user` | `Fn(u64) -> Pin<Box<dyn Future<Output = Option<PublicKeyBundle>> + Send>> + Send + Sync` (crypto) | Async lookup callback for login |
|
||||
| `complete_register` | `Fn(PublicKeyBundle) -> Pin<Box<dyn Future<Output = u64> + Send>> + Send + Sync` (crypto) | Async registration callback, returns new client ID |
|
||||
|
||||
### AuthenticationPolicy
|
||||
|
||||
`ForceAuthentication` requires every client to complete the login/register handshake. `AllowAuthentication` accepts both authenticated and unauthenticated connections — unauthenticated clients get a random ID and `AuthState::Unauthenticated`. `Unauthenticated` rejects any client that tries to authenticate and is the default.
|
||||
|
||||
```rust
|
||||
use mtp::host::AuthenticationPolicy;
|
||||
|
||||
// Force authentication (default was `require_authentication: true`):
|
||||
let config = HostConfig::new(ip, port, cert, key)
|
||||
.with_authentication(host_keyring, get_user, register);
|
||||
|
||||
// Allow both authenticated and unauthenticated:
|
||||
let config = HostConfig::new(ip, port, cert, key)
|
||||
.with_allow_authentication(host_keyring, get_user, register);
|
||||
|
||||
// Unauthenticated only (default):
|
||||
let config = HostConfig::new(ip, port, cert, key);
|
||||
```
|
||||
|
||||
### TLS
|
||||
|
||||
The host requires a TLS certificate. For development, generate a self-signed
|
||||
|
|
@ -82,6 +101,7 @@ pub struct MTPConnection {
|
|||
pub codec: VersionedCodec,
|
||||
pub sender: Sender,
|
||||
pub receiver: Receiver,
|
||||
pub description: Option<String>,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_state: AuthState,
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -95,6 +115,7 @@ pub struct MTPConnection {
|
|||
- `codec` -- a `VersionedCodec` scoped to the negotiated version (use for
|
||||
version-aware encode/decode)
|
||||
- `sender` / `receiver` -- for message I/O
|
||||
- `description` -- optional client-provided label (e.g. `"phone"`, `"desktop"`)
|
||||
- `client_id` -- the authenticated client's ID
|
||||
- `client_public_key` -- the client's public key bundle (for signature
|
||||
verification of subsequent messages)
|
||||
|
|
@ -130,7 +151,7 @@ let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
|
|||
|
||||
## Authentication Flow
|
||||
|
||||
When `require_authentication` is `true`, `accept()` runs a mutually-authenticated
|
||||
When `authentication_policy` is `ForceAuthentication`, `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
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
println!("Connecting to 127.0.0.1:8080 ...");
|
||||
|
||||
let config = ClientConfig::new("https://127.0.0.1:8080").with_pinned_pem(cert_pem);
|
||||
let config = ClientConfig::new("https://127.0.0.1:8080")
|
||||
.with_pinned_pem(cert_pem)
|
||||
.with_description("MTP example client");
|
||||
|
||||
let server_bundle = host_public_key.clone();
|
||||
let (conn, keyring) =
|
||||
|
|
|
|||
|
|
@ -98,8 +98,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
println!("Server listening on {}", host.local_addr());
|
||||
|
||||
while let Some(conn) = host.accept().await? {
|
||||
let desc = conn
|
||||
.description
|
||||
.as_deref()
|
||||
.unwrap_or("(no description)");
|
||||
println!(
|
||||
"\n--- New authenticated connection (version {}) ---",
|
||||
"\n--- New connection (version {}, description: {desc}) ---",
|
||||
conn.version
|
||||
);
|
||||
println!("Client ID: {}", conn.client_id);
|
||||
|
|
|
|||
388
host/src/lib.rs
388
host/src/lib.rs
|
|
@ -29,6 +29,14 @@ type CompleteRegister = Box<
|
|||
+ Sync,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuthenticationPolicy {
|
||||
ForceAuthentication,
|
||||
AllowAuthentication,
|
||||
Unauthenticated,
|
||||
}
|
||||
|
||||
/* Host configuration. */
|
||||
pub struct HostConfig {
|
||||
pub ip: IpAddr,
|
||||
|
|
@ -37,7 +45,7 @@ pub struct HostConfig {
|
|||
pub tls_key: Vec<u8>,
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
pub require_authentication: bool,
|
||||
pub authentication_policy: AuthenticationPolicy,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_timeout: Duration,
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -56,7 +64,7 @@ impl HostConfig {
|
|||
tls_fullchain,
|
||||
tls_key,
|
||||
#[cfg(feature = "crypto")]
|
||||
require_authentication: false,
|
||||
authentication_policy: AuthenticationPolicy::Unauthenticated,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_timeout: Duration::from_secs(30),
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -93,13 +101,19 @@ impl HostConfig {
|
|||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
self.require_authentication = true;
|
||||
self.authentication_policy = AuthenticationPolicy::ForceAuthentication;
|
||||
self.host_keyring = host_keyring;
|
||||
self.get_existing_user = Box::new(get_existing_user);
|
||||
self.complete_register = Box::new(complete_register);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
pub fn with_authentication_policy(mut self, policy: AuthenticationPolicy) -> Self {
|
||||
self.authentication_policy = policy;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.auth_timeout = timeout;
|
||||
|
|
@ -152,6 +166,7 @@ pub struct MTPConnection {
|
|||
pub codec: VersionedCodec,
|
||||
pub sender: Sender,
|
||||
pub receiver: Receiver,
|
||||
pub description: Option<String>,
|
||||
#[cfg(feature = "crypto")]
|
||||
pub auth_state: AuthState,
|
||||
#[cfg(feature = "crypto")]
|
||||
|
|
@ -203,49 +218,94 @@ impl MTPHost {
|
|||
};
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
if self.config.require_authentication {
|
||||
let timeout = self.config.auth_timeout;
|
||||
return match tokio::time::timeout(timeout, self.accept_authenticated(sender, receiver))
|
||||
match self.config.authentication_policy {
|
||||
AuthenticationPolicy::ForceAuthentication => {
|
||||
let timeout = self.config.auth_timeout;
|
||||
return match tokio::time::timeout(
|
||||
timeout,
|
||||
self.accept_authenticated(sender, receiver),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(AcceptError::AuthenticationTimedOut),
|
||||
};
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(AcceptError::AuthenticationTimedOut),
|
||||
};
|
||||
}
|
||||
AuthenticationPolicy::AllowAuthentication => {
|
||||
return self.accept_allow_auth(sender, receiver).await;
|
||||
}
|
||||
AuthenticationPolicy::Unauthenticated => {
|
||||
let first_msg = match receiver.receive().await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(AcceptError::Receive(e)),
|
||||
};
|
||||
if first_msg.get_type()
|
||||
== mtp_codec::CommunicationType::Register.to_id(&mtp_codec::TypeMap::latest())
|
||||
{
|
||||
sender.close();
|
||||
return Err(AcceptError::AuthenticationFailed(
|
||||
"authentication not allowed on this host".into(),
|
||||
));
|
||||
}
|
||||
let client_version = match extract_version(&first_msg) {
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::MissingVersion),
|
||||
};
|
||||
let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version))
|
||||
{
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||
};
|
||||
let codec = VersionedCodec::new(self.registry.clone());
|
||||
let description = match first_msg.get_data(DataType::Description) {
|
||||
DataValue::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
};
|
||||
Ok(Some(MTPConnection {
|
||||
version: negotiated,
|
||||
codec,
|
||||
sender,
|
||||
receiver,
|
||||
description,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_state: AuthState::Unauthenticated,
|
||||
#[cfg(feature = "crypto")]
|
||||
client_id: rand::random(),
|
||||
#[cfg(feature = "crypto")]
|
||||
client_public_key: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Read the first message (always encoded with reserved types).
|
||||
let first_msg = match receiver.receive().await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(AcceptError::Receive(e)),
|
||||
};
|
||||
|
||||
let client_version = match extract_version(&first_msg) {
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::MissingVersion),
|
||||
};
|
||||
|
||||
let negotiated = match self
|
||||
.registry
|
||||
.negotiate(std::slice::from_ref(&client_version))
|
||||
// Non-crypto fallback: no authentication feature, just read and respond.
|
||||
#[cfg(not(feature = "crypto"))]
|
||||
{
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||
};
|
||||
|
||||
let codec = VersionedCodec::new(self.registry.clone());
|
||||
|
||||
Ok(Some(MTPConnection {
|
||||
version: negotiated,
|
||||
codec,
|
||||
sender,
|
||||
receiver,
|
||||
#[cfg(feature = "crypto")]
|
||||
auth_state: AuthState::Unauthenticated,
|
||||
#[cfg(feature = "crypto")]
|
||||
client_id: 0,
|
||||
#[cfg(feature = "crypto")]
|
||||
client_public_key: None,
|
||||
}))
|
||||
let first_msg = match receiver.receive().await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(AcceptError::Receive(e)),
|
||||
};
|
||||
let client_version = match extract_version(&first_msg) {
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::MissingVersion),
|
||||
};
|
||||
let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version))
|
||||
{
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||
};
|
||||
let codec = VersionedCodec::new(self.registry.clone());
|
||||
let description = match first_msg.get_data(DataType::Description) {
|
||||
DataValue::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
};
|
||||
return Ok(Some(MTPConnection {
|
||||
version: negotiated,
|
||||
codec,
|
||||
sender,
|
||||
receiver,
|
||||
description,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> std::net::SocketAddr {
|
||||
|
|
@ -257,6 +317,18 @@ impl MTPHost {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
enum Flow {
|
||||
Login {
|
||||
id: u64,
|
||||
bundle: mtp_crypto::PublicKeyBundle,
|
||||
},
|
||||
Register {
|
||||
bundle: mtp_crypto::PublicKeyBundle,
|
||||
pk_bytes: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "crypto")]
|
||||
impl MTPHost {
|
||||
/*
|
||||
|
|
@ -278,51 +350,9 @@ impl MTPHost {
|
|||
sender: Sender,
|
||||
receiver: Receiver,
|
||||
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||
use mtp_crypto::{
|
||||
Ed25519Signer, MlDsaSigner, PublicKeyBundle, SignatureScheme, auth, verify_ed25519,
|
||||
verify_ml_dsa,
|
||||
};
|
||||
|
||||
// Flow-specific state resolved from the client's opening hello.
|
||||
enum Flow {
|
||||
Login {
|
||||
id: u64,
|
||||
bundle: PublicKeyBundle,
|
||||
},
|
||||
Register {
|
||||
bundle: PublicKeyBundle,
|
||||
pk_bytes: Vec<u8>,
|
||||
},
|
||||
}
|
||||
use mtp_crypto::PublicKeyBundle;
|
||||
|
||||
let tm = mtp_codec::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]| -> Result<(Vec<u8>, Vec<u8>), AcceptError> {
|
||||
let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||
let sig = signer
|
||||
.sign(payload)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||
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,
|
||||
)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||
pq.sign(payload)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok((sig, pq_sig))
|
||||
};
|
||||
|
||||
// ===== Step 1: receive the client's unsigned hello =====
|
||||
let hello = match receiver.receive().await {
|
||||
|
|
@ -347,6 +377,11 @@ impl MTPHost {
|
|||
}
|
||||
};
|
||||
|
||||
let description = match hello.get_data(DataType::Description) {
|
||||
DataValue::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let (flow, response_type) =
|
||||
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
||||
let cid = match hello.get_data(DataType::Id) {
|
||||
|
|
@ -400,6 +435,57 @@ impl MTPHost {
|
|||
));
|
||||
};
|
||||
|
||||
self.complete_auth_handshake(sender, receiver, flow, response_type, &version_str, client_version, description).await
|
||||
}
|
||||
|
||||
/*
|
||||
* Steps 2-4 of the authenticated handshake, shared by both ForceAuthentication
|
||||
* and AllowAuthentication. Takes the already-parsed hello (step 1) via `flow`,
|
||||
* `version_str`, and `client_version`.
|
||||
*/
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn complete_auth_handshake(
|
||||
&self,
|
||||
sender: Sender,
|
||||
receiver: Receiver,
|
||||
flow: Flow,
|
||||
response_type: mtp_codec::CommunicationType,
|
||||
version_str: &str,
|
||||
client_version: Version,
|
||||
description: Option<String>,
|
||||
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||
use mtp_crypto::{
|
||||
auth, verify_ed25519, verify_ml_dsa, Ed25519Signer, MlDsaSigner, SignatureScheme,
|
||||
};
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
let pq_enabled = !self
|
||||
.config
|
||||
.host_keyring
|
||||
.sig_pq_secret_key
|
||||
.as_bytes()
|
||||
.is_empty();
|
||||
|
||||
let host_sign = |payload: &[u8]| -> Result<(Vec<u8>, Vec<u8>), AcceptError> {
|
||||
let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||
let sig = signer
|
||||
.sign(payload)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||
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,
|
||||
)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||
pq.sign(payload)
|
||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok((sig, pq_sig))
|
||||
};
|
||||
|
||||
let challenge_id = match &flow {
|
||||
Flow::Login { id, .. } => *id,
|
||||
Flow::Register { .. } => 0,
|
||||
|
|
@ -464,18 +550,13 @@ impl MTPHost {
|
|||
|
||||
let (proof_payload, bundle) = match &flow {
|
||||
Flow::Login { id, bundle } => (
|
||||
auth::login_proof_payload(&version_str, *id, server_challenge, client_nonce),
|
||||
auth::login_proof_payload(version_str, *id, server_challenge, client_nonce),
|
||||
bundle,
|
||||
),
|
||||
Flow::Register {
|
||||
bundle, pk_bytes, ..
|
||||
} => (
|
||||
auth::register_proof_payload(
|
||||
&version_str,
|
||||
pk_bytes,
|
||||
server_challenge,
|
||||
client_nonce,
|
||||
),
|
||||
auth::register_proof_payload(version_str, pk_bytes, server_challenge, client_nonce),
|
||||
bundle,
|
||||
),
|
||||
};
|
||||
|
|
@ -551,11 +632,134 @@ impl MTPHost {
|
|||
codec,
|
||||
sender,
|
||||
receiver,
|
||||
description,
|
||||
auth_state: AuthState::Authenticated,
|
||||
client_id: assigned_id,
|
||||
client_public_key: Some(client_bundle),
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
* Allow-authentication accept: clients may connect with or without
|
||||
* authentication. Register messages always trigger the auth handshake.
|
||||
* Identification with a known client-id triggers login; otherwise the
|
||||
* client is treated as unauthenticated with a random id.
|
||||
*/
|
||||
async fn accept_allow_auth(
|
||||
&mut self,
|
||||
sender: Sender,
|
||||
receiver: Receiver,
|
||||
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||
use mtp_crypto::PublicKeyBundle;
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
|
||||
let hello = match receiver.receive().await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(AcceptError::Receive(e)),
|
||||
};
|
||||
|
||||
let version_str = match hello.get_data(DataType::Version) {
|
||||
DataValue::Str(s) => s.clone(),
|
||||
_ => {
|
||||
sender.close();
|
||||
return Err(AcceptError::MissingVersion);
|
||||
}
|
||||
};
|
||||
let client_version = match Version::parse(&version_str) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
sender.close();
|
||||
return Err(AcceptError::MissingVersion);
|
||||
}
|
||||
};
|
||||
|
||||
let description = match hello.get_data(DataType::Description) {
|
||||
DataValue::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Register → authenticated registration.
|
||||
if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
|
||||
let bundle = match hello.get_data(DataType::PublicKeys) {
|
||||
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).map_err(|_| {
|
||||
sender.close();
|
||||
AcceptError::AuthenticationFailed("invalid public key bundle".into())
|
||||
})?,
|
||||
_ => {
|
||||
sender.close();
|
||||
return Err(AcceptError::AuthenticationFailed("missing public keys".into()));
|
||||
}
|
||||
};
|
||||
let pk_bytes = bundle.as_bytes();
|
||||
return self
|
||||
.complete_auth_handshake(
|
||||
sender,
|
||||
receiver,
|
||||
Flow::Register {
|
||||
bundle,
|
||||
pk_bytes,
|
||||
},
|
||||
mtp_codec::CommunicationType::RegisterResponse,
|
||||
&version_str,
|
||||
client_version,
|
||||
description,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Identification with a known client-id → login.
|
||||
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
||||
let cid = match hello.get_data(DataType::Id) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if cid > 0
|
||||
&& let Some(bundle) = (self.config.get_existing_user)(cid).await
|
||||
{
|
||||
return self
|
||||
.complete_auth_handshake(
|
||||
sender,
|
||||
receiver,
|
||||
Flow::Login {
|
||||
id: cid,
|
||||
bundle,
|
||||
},
|
||||
mtp_codec::CommunicationType::IdentificationResponse,
|
||||
&version_str,
|
||||
client_version,
|
||||
description,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Unknown (or zero) client id → unauthenticated connection.
|
||||
let negotiated = match self
|
||||
.registry
|
||||
.negotiate(std::slice::from_ref(&client_version))
|
||||
{
|
||||
Some(v) => v,
|
||||
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||
};
|
||||
let codec = VersionedCodec::new(self.registry.clone());
|
||||
return Ok(Some(MTPConnection {
|
||||
version: negotiated,
|
||||
codec,
|
||||
sender,
|
||||
receiver,
|
||||
description,
|
||||
auth_state: AuthState::Unauthenticated,
|
||||
client_id: rand::random(),
|
||||
client_public_key: None,
|
||||
}));
|
||||
}
|
||||
|
||||
sender.close();
|
||||
Err(AcceptError::AuthenticationFailed(
|
||||
"unexpected message type".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -302,12 +302,15 @@ impl WasmClient {
|
|||
.await?;
|
||||
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
let ident = CommunicationValue::new(CommunicationType::Identification)
|
||||
let mut ident = CommunicationValue::new(CommunicationType::Identification)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
.add_typed_default(
|
||||
DataType::Id,
|
||||
DataValue::UnsignedNumber(config.client_id as u128),
|
||||
);
|
||||
if let Some(desc) = &config.description {
|
||||
ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||
}
|
||||
let ident_bytes = ident
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
|
|
@ -351,12 +354,16 @@ impl WasmClient {
|
|||
.await?;
|
||||
|
||||
// 1. Send the unsigned Identification hello.
|
||||
let hello = CommunicationValue::new(CommunicationType::Identification)
|
||||
let mut 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))
|
||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128));
|
||||
if let Some(desc) = &config.description {
|
||||
hello = hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||
}
|
||||
let hello_bytes = hello
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&hello).await?;
|
||||
transport.send_frame(&hello_bytes).await?;
|
||||
|
||||
// 2. Receive and verify the host's challenge.
|
||||
let server_challenge = self
|
||||
|
|
@ -463,12 +470,16 @@ impl WasmClient {
|
|||
.await?;
|
||||
|
||||
// 1. Send the unsigned Register hello (version + public-key bundle).
|
||||
let hello = CommunicationValue::new(CommunicationType::Register)
|
||||
let mut 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()))
|
||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
|
||||
if let Some(desc) = &config.description {
|
||||
hello = hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||
}
|
||||
let hello_bytes = hello
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
transport.send_frame(&hello).await?;
|
||||
transport.send_frame(&hello_bytes).await?;
|
||||
|
||||
// 2. Receive and verify the host's challenge (register binds id = 0).
|
||||
let server_challenge = self
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ pub struct ConnectionConfig {
|
|||
pub(crate) server_certificate_hashes: Option<Vec<String>>,
|
||||
pub(crate) client_id: u64,
|
||||
pub(crate) max_message_size: u32,
|
||||
pub(crate) description: Option<String>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
|
|
@ -17,6 +18,7 @@ impl ConnectionConfig {
|
|||
server_certificate_hashes: None,
|
||||
client_id: 0,
|
||||
max_message_size: 1_000_000_000,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,4 +51,14 @@ impl ConnectionConfig {
|
|||
pub fn max_message_size(&self) -> u32 {
|
||||
self.max_message_size
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_description(&mut self, description: String) {
|
||||
self.description = Some(description);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn description(&self) -> Option<String> {
|
||||
self.description.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue