[WIP] Security work While on holiday
This commit is contained in:
parent
a81ac4efca
commit
7f0231e3f1
109 changed files with 19694 additions and 5210 deletions
39
example/Cargo.lock
generated
39
example/Cargo.lock
generated
|
|
@ -12,6 +12,18 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures 0.2.17",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.2"
|
||||
|
|
@ -125,6 +137,15 @@ version = "2.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
|
@ -427,6 +448,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1263,6 +1285,7 @@ dependencies = [
|
|||
"mtp-crypto",
|
||||
"mtp-type-map",
|
||||
"rand 0.10.2",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1287,7 +1310,7 @@ dependencies = [
|
|||
"ml-dsa",
|
||||
"mlkem-tls",
|
||||
"rand 0.10.2",
|
||||
"rand_core 0.10.1",
|
||||
"rand_core 0.6.4",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"serde",
|
||||
|
|
@ -1302,6 +1325,7 @@ dependencies = [
|
|||
name = "mtp-files"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"mtp-crypto",
|
||||
"rand 0.10.2",
|
||||
"thiserror 1.0.69",
|
||||
|
|
@ -1330,6 +1354,7 @@ dependencies = [
|
|||
"mtp-codec",
|
||||
"mtp-common",
|
||||
"mtp-crypto",
|
||||
"rand 0.10.2",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
|
|
@ -1337,6 +1362,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tracing",
|
||||
"wtransport",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1484,6 +1510,17 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ name = "client"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["client", "crypto", "files", "pipes"] }
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["client", "crypto", "files", "pipes", "raw"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
rand = "0.10.1"
|
||||
tracing-subscriber = "0.3.23"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ use std::time::{Duration, Instant};
|
|||
use tokio::fs;
|
||||
|
||||
use mtp::client::{ClientConfig, MTPClient, MTPConnection};
|
||||
use mtp::crypto::{
|
||||
Ed25519Signer, KemPrivateKey, KemPublicKey, Keyring, MlDsaSigner, PublicKeyBundle,
|
||||
};
|
||||
use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||
use mtp::files::{load_keyring_raw, save_keyring_raw};
|
||||
|
||||
pub async fn connect_or_register(
|
||||
|
|
@ -40,17 +38,8 @@ pub async fn connect_or_register(
|
|||
|
||||
println!("No existing keys found: registering new client");
|
||||
|
||||
/* The client authenticates with signatures only, so the KEM slot is empty. */
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let keyring = Keyring::new(
|
||||
KemPublicKey::new(vec![]),
|
||||
KemPrivateKey::new(vec![]),
|
||||
sig_pq_pk,
|
||||
sig_pq_sk,
|
||||
sig_pk,
|
||||
sig_sk,
|
||||
);
|
||||
/* Registration publishes a complete MTP identity for later protection. */
|
||||
let keyring = Keyring::generate();
|
||||
|
||||
let reg_started = Instant::now();
|
||||
let conn = MTPClient::auth_register(config, &keyring, &host_public_key).await?;
|
||||
|
|
@ -63,3 +52,19 @@ pub async fn connect_or_register(
|
|||
|
||||
Ok((conn, keyring, "register".into(), reg_duration))
|
||||
}
|
||||
|
||||
/// Open a guest transport even when the caller already owns registered
|
||||
/// credentials. The credentials stay with the caller for protected signing.
|
||||
pub async fn connect_unauthenticated(
|
||||
config: ClientConfig,
|
||||
) -> Result<MTPConnection, Box<dyn std::error::Error>> {
|
||||
let conn = MTPClient::connect(config).await?;
|
||||
if conn.auth_state != mtp::client::AuthState::Unauthenticated {
|
||||
return Err("guest connection did not report Unauthenticated state".into());
|
||||
}
|
||||
println!(
|
||||
"Opened unauthenticated transport with host-assigned guest ID {}",
|
||||
conn.client_id
|
||||
);
|
||||
Ok(conn)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ mod auth;
|
|||
mod metrics;
|
||||
mod messages;
|
||||
mod pipes;
|
||||
mod protected;
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use mtp::client::ClientConfig;
|
||||
use mtp::client::{AuthState, ClientConfig};
|
||||
use mtp::files::load_public_key_bundle;
|
||||
|
||||
fn dev_cert_path() -> String {
|
||||
|
|
@ -43,7 +44,7 @@ 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)
|
||||
.with_pinned_pem(cert_pem.clone())
|
||||
.with_description("MTP example client");
|
||||
|
||||
let server_bundle = host_public_key.clone();
|
||||
|
|
@ -62,9 +63,57 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
let mut builder = metrics::SessionBuilder::new(&auth_method, auth_duration);
|
||||
|
||||
if conn.auth_state != AuthState::Authenticated {
|
||||
return Err("authenticated example connection did not report Authenticated state".into());
|
||||
}
|
||||
println!(
|
||||
"Receive connection A: authenticated client {}",
|
||||
conn.client_id
|
||||
);
|
||||
|
||||
let unauthenticated_config = ClientConfig::new("https://127.0.0.1:8080")
|
||||
.with_pinned_pem(cert_pem.clone())
|
||||
.with_description("MTP example unauthenticated sender");
|
||||
let unauthenticated_conn = auth::connect_unauthenticated(unauthenticated_config).await?;
|
||||
println!(
|
||||
"Send connection B: unauthenticated guest transport ID {}",
|
||||
unauthenticated_conn.client_id
|
||||
);
|
||||
|
||||
let direct_roundtrip = protected::send_direct_protected(
|
||||
&unauthenticated_conn,
|
||||
conn.client_id,
|
||||
&keyring,
|
||||
&server_bundle,
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"Protected signer {} was accepted through unauthenticated connection B",
|
||||
conn.client_id
|
||||
);
|
||||
|
||||
let relay_roundtrip = protected::send_sealed_relay(
|
||||
&unauthenticated_conn,
|
||||
conn.client_id,
|
||||
&keyring,
|
||||
&server_bundle,
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"Sealed relay round-trip completed in {:.3}ms",
|
||||
relay_roundtrip.as_secs_f64() * 1000.0
|
||||
);
|
||||
|
||||
unauthenticated_conn.sender.close().await;
|
||||
|
||||
let roundtrip = messages::send_and_receive(&conn, &keyring, &server_bundle).await?;
|
||||
builder.set_message_roundtrip(roundtrip);
|
||||
|
||||
println!(
|
||||
"Direct protected round-trip: {:.3}ms",
|
||||
direct_roundtrip.as_secs_f64() * 1000.0
|
||||
);
|
||||
|
||||
println!("\n--- Pipe demo ---");
|
||||
let pipe_results = pipes::run_pipe_demo(&conn, 1).await?;
|
||||
for result in &pipe_results {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use mtp::client::MTPConnection;
|
||||
use mtp::codec::ProtectionPurpose;
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use mtp::crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm};
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, PublicKeyBundle};
|
||||
use mtp::type_map::TypeMap;
|
||||
|
||||
pub fn build_demo_message(
|
||||
|
|
@ -11,7 +12,6 @@ pub fn build_demo_message(
|
|||
server_bundle: &PublicKeyBundle,
|
||||
) -> Result<CommunicationValue, Box<dyn std::error::Error>> {
|
||||
// Encrypt to the server's KEM public key; the server decrypts with its keyring.
|
||||
let enc_type = EncryptionType::MlKemChaCha20Poly1305;
|
||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)?;
|
||||
|
||||
let tm = TypeMap::latest();
|
||||
|
|
@ -26,15 +26,16 @@ pub fn build_demo_message(
|
|||
(version_id, DataValue::Str("secret inner data".into())),
|
||||
(id_id, DataValue::UnsignedNumber(42)),
|
||||
]);
|
||||
let mut dv_enc = inner_enc;
|
||||
dv_enc.encrypt_container(enc_type, server_bundle, b"demo-aad");
|
||||
let dv_enc = inner_enc.encrypt_for(
|
||||
std::slice::from_ref(server_bundle),
|
||||
ProtectionPurpose::from(1),
|
||||
)?;
|
||||
|
||||
let inner_sig = DataValue::Container(vec![
|
||||
(version_id, DataValue::Str("signed by client".into())),
|
||||
(id_id, DataValue::UnsignedNumber(99)),
|
||||
]);
|
||||
let mut dv_sig = inner_sig;
|
||||
dv_sig.sign_container(SigAlgorithm::ED25519, &signer);
|
||||
let dv_sig = inner_sig.sign(client_id, ProtectionPurpose::from(2), &signer)?;
|
||||
|
||||
let inner_sec = DataValue::Container(vec![
|
||||
(
|
||||
|
|
@ -43,18 +44,16 @@ pub fn build_demo_message(
|
|||
),
|
||||
(id_id, 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",
|
||||
);
|
||||
let dv_sec = inner_sec
|
||||
.sign(client_id, ProtectionPurpose::from(3), &signer)?
|
||||
.encrypt_for(
|
||||
std::slice::from_ref(server_bundle),
|
||||
ProtectionPurpose::from(4),
|
||||
)?;
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_secs();
|
||||
.as_millis();
|
||||
|
||||
let msg = CommunicationValue::new(CommunicationType::Ping)
|
||||
.add_typed_default(
|
||||
|
|
@ -101,7 +100,10 @@ pub async fn send_and_receive(
|
|||
Ok(resp) => {
|
||||
let roundtrip = start.elapsed();
|
||||
println!("Received: {resp}");
|
||||
println!("Message round-trip: {:.3}ms", roundtrip.as_secs_f64() * 1000.0);
|
||||
println!(
|
||||
"Message round-trip: {:.3}ms",
|
||||
roundtrip.as_secs_f64() * 1000.0
|
||||
);
|
||||
Ok(roundtrip)
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use mtp::common::unix_time_millis;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
|
@ -8,6 +9,9 @@ fn now_epoch_secs() -> u64 {
|
|||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
fn now_epoch_millis() -> u64 {
|
||||
unix_time_millis().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn generate_session_id() -> String {
|
||||
let ts = now_epoch_secs();
|
||||
|
|
@ -197,7 +201,7 @@ impl SessionBuilder {
|
|||
pub fn new(auth_method: &str, auth_duration: Duration) -> Self {
|
||||
Self {
|
||||
session_id: generate_session_id(),
|
||||
timestamp: now_epoch_secs(),
|
||||
timestamp: now_epoch_millis(),
|
||||
auth_method: auth_method.to_string(),
|
||||
auth_duration_ms: auth_duration.as_secs_f64() * 1000.0,
|
||||
error: None,
|
||||
|
|
@ -219,11 +223,7 @@ impl SessionBuilder {
|
|||
}
|
||||
|
||||
pub fn build(self) -> ClientSessionRecord {
|
||||
let total_pipe_bytes: u64 = self
|
||||
.pipe_results
|
||||
.iter()
|
||||
.map(|r| r.size as u64)
|
||||
.sum();
|
||||
let total_pipe_bytes: u64 = self.pipe_results.iter().map(|r| r.size as u64).sum();
|
||||
|
||||
let overall_pipe_avg_total_ms = if self.pipe_results.is_empty() {
|
||||
0.0
|
||||
|
|
@ -235,7 +235,10 @@ impl SessionBuilder {
|
|||
let overall_pipe_avg_data_ms = if self.pipe_results.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
self.pipe_results.iter().map(|r| r.data_only_ms).sum::<f64>()
|
||||
self.pipe_results
|
||||
.iter()
|
||||
.map(|r| r.data_only_ms)
|
||||
.sum::<f64>()
|
||||
/ self.pipe_results.len() as f64
|
||||
};
|
||||
|
||||
|
|
|
|||
198
example/client/src/protected.rs
Normal file
198
example/client/src/protected.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use mtp::client::MTPConnection;
|
||||
use mtp::codec::{
|
||||
CommunicationType, DataType, DataValue, ProtectionPolicy, ProtectedMessageBuilder,
|
||||
ProtectionPurpose, SealedRelayBuilder, SignaturePolicy, TypeMap, open_relay_content,
|
||||
open_relay_metadata,
|
||||
};
|
||||
use mtp::common::unix_time_millis;
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, PublicKeyBundle};
|
||||
|
||||
/// The direct protected example sends to the host as the destination MTP ID.
|
||||
pub const DIRECT_DESTINATION_ID: u64 = 1;
|
||||
|
||||
/// The example host acts as the metadata relay and uses this stable MTP ID.
|
||||
pub const METADATA_RELAY_ID: u64 = 1;
|
||||
|
||||
/// This keyring represents a final recipient independently of the transport
|
||||
/// identity used by the example client.
|
||||
pub const FINAL_RECIPIENT_ID: u64 = 7_002;
|
||||
|
||||
const DIRECT_SIGNATURE_PURPOSE: u8 = 0x40;
|
||||
const DIRECT_ENCRYPTION_PURPOSE: u8 = 0x41;
|
||||
const RELAY_SIGNATURE_POLICY: ProtectionPolicy = ProtectionPolicy {
|
||||
signature: SignaturePolicy::Ed25519,
|
||||
};
|
||||
|
||||
fn type_id(
|
||||
data_type: DataType,
|
||||
type_map: &TypeMap,
|
||||
) -> Result<mtp::codec::DataTypeId, Box<dyn std::error::Error>> {
|
||||
data_type
|
||||
.try_to_id(type_map)
|
||||
.ok_or_else(|| format!("missing example data type mapping for {data_type}").into())
|
||||
}
|
||||
|
||||
fn application_value(text: &str, number: u128) -> Result<DataValue, Box<dyn std::error::Error>> {
|
||||
let type_map = TypeMap::latest();
|
||||
Ok(DataValue::Container(vec![
|
||||
(
|
||||
type_id(DataType::ExampleText, &type_map)?,
|
||||
DataValue::Str(text.to_owned()),
|
||||
),
|
||||
(
|
||||
type_id(DataType::ExampleNumber, &type_map)?,
|
||||
DataValue::UnsignedNumber(number),
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
fn relay_metadata() -> Result<DataValue, Box<dyn std::error::Error>> {
|
||||
let type_map = TypeMap::latest();
|
||||
Ok(DataValue::Container(vec![
|
||||
(
|
||||
type_id(DataType::ExampleRole, &type_map)?,
|
||||
DataValue::Str("metadata relay".into()),
|
||||
),
|
||||
(
|
||||
type_id(DataType::ExampleMetadata, &type_map)?,
|
||||
DataValue::Str("application metadata remains authenticated and opaque to MTP".into()),
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
/// Send an application value directly to the host without constructing a
|
||||
/// Relay frame. The outer sender is deliberately absent so the example also
|
||||
/// demonstrates that the protected signer is independent of transport auth.
|
||||
pub async fn send_direct_protected(
|
||||
conn: &MTPConnection,
|
||||
signer_id: u64,
|
||||
signer_keyring: &Keyring,
|
||||
recipient_public_key: &PublicKeyBundle,
|
||||
) -> Result<Duration, Box<dyn std::error::Error>> {
|
||||
let signer = Ed25519Signer::new(&signer_keyring.sig_cl_secret_key)?;
|
||||
let created_at = unix_time_millis()?;
|
||||
let message_id = format!(
|
||||
"example-direct-{created_at}-{}",
|
||||
rand::random::<u32>()
|
||||
);
|
||||
let content = application_value("direct protected delivery", 40)?;
|
||||
let frame = ProtectedMessageBuilder::new(
|
||||
"ProtectedMessage",
|
||||
content,
|
||||
signer_id,
|
||||
DIRECT_DESTINATION_ID,
|
||||
&signer,
|
||||
ProtectionPurpose::from(DIRECT_SIGNATURE_PURPOSE),
|
||||
ProtectionPurpose::from(DIRECT_ENCRYPTION_PURPOSE),
|
||||
)
|
||||
.message_id(message_id)
|
||||
.created_at(created_at)
|
||||
.recipients(vec![recipient_public_key.clone()])
|
||||
.type_map(&TypeMap::latest())
|
||||
.build()?;
|
||||
|
||||
println!(
|
||||
"Sending direct protected frame: type=ProtectedMessage receiver={} outer_sender=absent signer={signer_id}",
|
||||
DIRECT_DESTINATION_ID
|
||||
);
|
||||
let started = Instant::now();
|
||||
conn.sender.send(&frame).await?;
|
||||
let response = conn.receive().await?;
|
||||
if !response.is_type(CommunicationType::Pong) {
|
||||
return Err(format!("direct protected response was not Pong: {response}").into());
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
println!(
|
||||
"Direct protected value verified and acknowledged in {:.3}ms",
|
||||
elapsed.as_secs_f64() * 1000.0
|
||||
);
|
||||
Ok(elapsed)
|
||||
}
|
||||
|
||||
/// Send a sealed relay through the host, which can open metadata but cannot
|
||||
/// decrypt the content. The final recipient is represented by a separate
|
||||
/// keyring so the example does not conflate relay and content access.
|
||||
pub async fn send_sealed_relay(
|
||||
conn: &MTPConnection,
|
||||
signer_id: u64,
|
||||
signer_keyring: &Keyring,
|
||||
metadata_relay_public_key: &PublicKeyBundle,
|
||||
) -> Result<Duration, Box<dyn std::error::Error>> {
|
||||
let type_map = TypeMap::latest();
|
||||
let final_recipient_keyring = Keyring::generate();
|
||||
let final_recipient_public_key = final_recipient_keyring.public_key_bundle();
|
||||
let signer = Ed25519Signer::new(&signer_keyring.sig_cl_secret_key)?;
|
||||
let created_at = unix_time_millis()?;
|
||||
let message_id = format!("example-relay-{created_at}-{}", rand::random::<u32>());
|
||||
let frame = SealedRelayBuilder::new(
|
||||
"ProtectedMessage",
|
||||
application_value("sealed relay delivery", 41)?,
|
||||
signer_id,
|
||||
FINAL_RECIPIENT_ID,
|
||||
METADATA_RELAY_ID,
|
||||
&signer,
|
||||
)
|
||||
.message_id(message_id)
|
||||
.created_at(created_at)
|
||||
.metadata(relay_metadata()?)
|
||||
.metadata_recipients(vec![
|
||||
metadata_relay_public_key.clone(),
|
||||
final_recipient_public_key.clone(),
|
||||
])
|
||||
.content_recipients(vec![final_recipient_public_key])
|
||||
.type_map(&type_map)
|
||||
.build()?;
|
||||
|
||||
println!(
|
||||
"Sending sealed relay: next_hop={} final_recipient={} metadata_recipients=2 content_recipients=1",
|
||||
METADATA_RELAY_ID, FINAL_RECIPIENT_ID
|
||||
);
|
||||
let started = Instant::now();
|
||||
conn.sender.send(&frame).await?;
|
||||
let forwarded = conn.receive().await?;
|
||||
if !forwarded.is_type(CommunicationType::Relay) {
|
||||
return Err(format!("relay response was not Relay: {forwarded}").into());
|
||||
}
|
||||
if forwarded.sender().is_some() || forwarded.receiver() != Some(FINAL_RECIPIENT_ID) {
|
||||
return Err("relay forwarding changed the sealed-sender boundary".into());
|
||||
}
|
||||
|
||||
let metadata = open_relay_metadata(
|
||||
&forwarded,
|
||||
&final_recipient_keyring,
|
||||
signer_id,
|
||||
&signer_keyring.public_key_bundle(),
|
||||
RELAY_SIGNATURE_POLICY,
|
||||
)?;
|
||||
let application_metadata = metadata
|
||||
.metadata()
|
||||
.ok_or("forwarded relay metadata was missing")?;
|
||||
let content = open_relay_content(
|
||||
&metadata,
|
||||
&final_recipient_keyring,
|
||||
&signer_keyring.public_key_bundle(),
|
||||
FINAL_RECIPIENT_ID,
|
||||
RELAY_SIGNATURE_POLICY,
|
||||
)?;
|
||||
if content.message_type != "ProtectedMessage" {
|
||||
return Err(format!("unexpected relay message type: {}", content.message_type).into());
|
||||
}
|
||||
let expected_metadata = relay_metadata()?;
|
||||
if application_metadata != &expected_metadata {
|
||||
return Err("relay application metadata changed during forwarding".into());
|
||||
}
|
||||
let expected_content = application_value("sealed relay delivery", 41)?;
|
||||
if content.content != expected_content {
|
||||
return Err("relay application content changed during forwarding".into());
|
||||
}
|
||||
|
||||
let elapsed = started.elapsed();
|
||||
println!(
|
||||
"Final recipient opened authenticated metadata and content in {:.3}ms (message_id={})",
|
||||
elapsed.as_secs_f64() * 1000.0,
|
||||
metadata.message_id()
|
||||
);
|
||||
Ok(elapsed)
|
||||
}
|
||||
|
|
@ -4,4 +4,4 @@ version = "0.2.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["files"] }
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["files", "raw"] }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ name = "server"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["crypto", "tls", "web-server", "files", "pipes"] }
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["crypto", "tls", "web-server", "files", "pipes", "raw"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
http = "1"
|
||||
serde_json = { version = "1" }
|
||||
|
|
|
|||
|
|
@ -1,23 +1,208 @@
|
|||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||
use mtp::crypto::{CryptoError, Keyring, SignaturePublicKey, SignatureScheme, verify_ed25519};
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct Ed25519Verifier(SignaturePublicKey);
|
||||
use mtp::codec::{
|
||||
CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, InMemoryReplayGuard,
|
||||
ProtectionPolicy, ProtectionPurpose, SignaturePolicy, TypeMap,
|
||||
forward_relay_frame, open_protected_with, open_relay_content, open_relay_metadata_with,
|
||||
};
|
||||
use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||
|
||||
impl SignatureScheme for Ed25519Verifier {
|
||||
fn sign(&self, _msg: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
Err(CryptoError::SigningFailed)
|
||||
const DIRECT_DESTINATION_ID: u64 = 1;
|
||||
const METADATA_RELAY_ID: u64 = 1;
|
||||
const FINAL_RECIPIENT_ID: u64 = 7_002;
|
||||
const DIRECT_SIGNATURE_PURPOSE: u8 = 0x40;
|
||||
const DIRECT_ENCRYPTION_PURPOSE: u8 = 0x41;
|
||||
const SIGNATURE_POLICY: ProtectionPolicy = ProtectionPolicy {
|
||||
signature: SignaturePolicy::Ed25519,
|
||||
};
|
||||
|
||||
fn resolve_signer_key(
|
||||
signer_id: u64,
|
||||
registered_clients: &HashMap<u64, PublicKeyBundle>,
|
||||
) -> Option<PublicKeyBundle> {
|
||||
registered_clients.get(&signer_id).cloned()
|
||||
}
|
||||
|
||||
fn pong(tm: &TypeMap, data: impl Into<String>) -> Result<CommunicationValue, String> {
|
||||
let desc_id = DataTypeId(
|
||||
tm.data_id_enum(DataType::Description)
|
||||
.ok_or("missing Description type mapping")?,
|
||||
);
|
||||
let ts_id = DataTypeId(
|
||||
tm.data_id_enum(DataType::Timestamp)
|
||||
.ok_or("missing Timestamp type mapping")?,
|
||||
);
|
||||
let data_id = DataTypeId(
|
||||
tm.data_id_enum(DataType::Data)
|
||||
.ok_or("missing Data type mapping")?,
|
||||
);
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| e.to_string())?
|
||||
.as_millis();
|
||||
|
||||
CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||
.add_data(desc_id, DataValue::Str("MTP example response".into()))
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(ts_id, DataValue::UnsignedNumber(now as u128))
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(data_id, DataValue::Str(data.into()))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn process_direct_protected(
|
||||
msg: &CommunicationValue,
|
||||
tm: &TypeMap,
|
||||
client_pk: Option<&PublicKeyBundle>,
|
||||
registered_clients: &HashMap<u64, PublicKeyBundle>,
|
||||
host_keyring: &Keyring,
|
||||
accepted_messages: &mut InMemoryReplayGuard,
|
||||
) -> Result<CommunicationValue, String> {
|
||||
if msg.receiver() != Some(DIRECT_DESTINATION_ID) {
|
||||
return Err(format!(
|
||||
"direct protected frame was addressed to {:?}, expected destination {DIRECT_DESTINATION_ID}",
|
||||
msg.receiver()
|
||||
));
|
||||
}
|
||||
fn verify(&self, msg: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
|
||||
verify_ed25519(&self.0, msg, signature)
|
||||
|
||||
let opened = open_protected_with(
|
||||
msg,
|
||||
std::slice::from_ref(&host_keyring),
|
||||
None,
|
||||
|signer_id| resolve_signer_key(signer_id, registered_clients).map(|key| vec![key]),
|
||||
Some(DIRECT_DESTINATION_ID),
|
||||
ProtectionPurpose::from(DIRECT_SIGNATURE_PURPOSE),
|
||||
ProtectionPurpose::from(DIRECT_ENCRYPTION_PURPOSE),
|
||||
SIGNATURE_POLICY,
|
||||
Some(accepted_messages),
|
||||
)
|
||||
.map_err(|e| format!("direct protected message could not be authenticated: {e}"))?;
|
||||
let signer_id = opened.signer_id;
|
||||
let message_id = opened.message_id;
|
||||
let value = opened
|
||||
.content
|
||||
.as_container()
|
||||
.ok_or("direct protected application value is not a container")?;
|
||||
|
||||
let text_id = DataTypeId(
|
||||
tm.data_id_enum(DataType::ExampleText)
|
||||
.ok_or("missing ExampleText type mapping")?,
|
||||
);
|
||||
let number_id = DataTypeId(
|
||||
tm.data_id_enum(DataType::ExampleNumber)
|
||||
.ok_or("missing ExampleNumber type mapping")?,
|
||||
);
|
||||
let text = value
|
||||
.iter()
|
||||
.find(|(id, _)| *id == text_id)
|
||||
.and_then(|(_, value)| value.as_str())
|
||||
.ok_or("direct protected value is missing ExampleText")?;
|
||||
let number = value
|
||||
.iter()
|
||||
.find(|(id, _)| *id == number_id)
|
||||
.and_then(|(_, value)| value.as_unsigned_number())
|
||||
.ok_or("direct protected value is missing ExampleNumber")?;
|
||||
|
||||
println!(
|
||||
" Direct protected message: signer={signer_id}, message_id={message_id}, transport_key_available={}, ExampleText={text:?}, ExampleNumber={number}",
|
||||
client_pk.is_some()
|
||||
);
|
||||
if client_pk.is_none() {
|
||||
println!(
|
||||
" Protected signer was verified from the registered key map; transport is unauthenticated"
|
||||
);
|
||||
}
|
||||
pong(
|
||||
tm,
|
||||
format!("direct protected value verified for signer {signer_id}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn process_sealed_relay(
|
||||
msg: &CommunicationValue,
|
||||
registered_clients: &HashMap<u64, PublicKeyBundle>,
|
||||
host_keyring: &Keyring,
|
||||
accepted_messages: &mut InMemoryReplayGuard,
|
||||
) -> Result<CommunicationValue, String> {
|
||||
if msg.receiver() != Some(METADATA_RELAY_ID) {
|
||||
return Err(format!(
|
||||
"sealed relay next hop was {:?}, expected metadata relay {METADATA_RELAY_ID}",
|
||||
msg.receiver()
|
||||
));
|
||||
}
|
||||
|
||||
let metadata = open_relay_metadata_with(
|
||||
msg,
|
||||
std::slice::from_ref(&host_keyring),
|
||||
None,
|
||||
|signer_id| {
|
||||
resolve_signer_key(signer_id, registered_clients).map(|key| vec![key])
|
||||
},
|
||||
SIGNATURE_POLICY,
|
||||
Some(accepted_messages),
|
||||
)
|
||||
.map_err(|e| format!("metadata relay could not authenticate metadata: {e}"))?;
|
||||
println!(
|
||||
" Metadata relay opened message_id={} signer={} final_recipient={} metadata={:?}",
|
||||
metadata.message_id(),
|
||||
metadata.signer_id(),
|
||||
metadata.final_recipient_id(),
|
||||
metadata.metadata()
|
||||
);
|
||||
println!(
|
||||
" Metadata relay retained opaque encrypted content ({} bytes)",
|
||||
metadata
|
||||
.encrypted_content()
|
||||
.to_bytes()
|
||||
.map_err(|e| format!("opaque content serialization failed: {e}"))?
|
||||
.len()
|
||||
);
|
||||
|
||||
let content_result = open_relay_content(
|
||||
&metadata,
|
||||
host_keyring,
|
||||
&resolve_signer_key(metadata.signer_id(), registered_clients)
|
||||
.ok_or("metadata signer key disappeared")?,
|
||||
FINAL_RECIPIENT_ID,
|
||||
SIGNATURE_POLICY,
|
||||
);
|
||||
if content_result.is_ok() {
|
||||
return Err("metadata relay unexpectedly decrypted final-recipient content".into());
|
||||
}
|
||||
println!(" Metadata relay cannot decrypt final-recipient content (expected)");
|
||||
|
||||
forward_relay_frame(msg, metadata.final_recipient_id())
|
||||
.map_err(|e| format!("metadata relay forwarding failed: {e}"))
|
||||
}
|
||||
|
||||
pub fn process_and_respond(
|
||||
msg: &CommunicationValue,
|
||||
tm: &TypeMap,
|
||||
client_pk: Option<&mtp::crypto::PublicKeyBundle>,
|
||||
registered_clients: &HashMap<u64, PublicKeyBundle>,
|
||||
host_keyring: &Keyring,
|
||||
accepted_direct_messages: &mut InMemoryReplayGuard,
|
||||
accepted_relay_messages: &mut InMemoryReplayGuard,
|
||||
) -> Result<CommunicationValue, String> {
|
||||
if msg.is_type(CommunicationType::ProtectedMessage) {
|
||||
return process_direct_protected(
|
||||
msg,
|
||||
tm,
|
||||
client_pk,
|
||||
registered_clients,
|
||||
host_keyring,
|
||||
accepted_direct_messages,
|
||||
);
|
||||
}
|
||||
if msg.is_type(CommunicationType::Relay) {
|
||||
return process_sealed_relay(
|
||||
msg,
|
||||
registered_clients,
|
||||
host_keyring,
|
||||
accepted_relay_messages,
|
||||
);
|
||||
}
|
||||
|
||||
let desc_id = DataTypeId(
|
||||
tm.data_id_enum(DataType::Description)
|
||||
.ok_or("missing Description type mapping")?,
|
||||
|
|
@ -59,13 +244,34 @@ pub fn process_and_respond(
|
|||
.ok_or("missing SecurePayload type mapping")?,
|
||||
);
|
||||
|
||||
let description = msg.get_data(DataType::Description);
|
||||
let timestamp = msg.get_data(DataType::Timestamp);
|
||||
let data = msg.get_data(DataType::Data);
|
||||
let flags = msg.get_data(DataType::Flags);
|
||||
let value = msg.get_data(DataType::Value);
|
||||
let binary = msg.get_data(DataType::BinaryData);
|
||||
let items = msg.get_data(DataType::Items);
|
||||
let description = msg
|
||||
.get_data(DataType::Description)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
let timestamp = msg
|
||||
.get_data(DataType::Timestamp)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
let data = msg
|
||||
.get_data(DataType::Data)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
let flags = msg
|
||||
.get_data(DataType::Flags)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
let value = msg
|
||||
.get_data(DataType::Value)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
let binary = msg
|
||||
.get_data(DataType::BinaryData)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
let items = msg
|
||||
.get_data(DataType::Items)
|
||||
.cloned()
|
||||
.unwrap_or(DataValue::Null);
|
||||
|
||||
println!(
|
||||
" Description: {}",
|
||||
|
|
@ -82,13 +288,8 @@ pub fn process_and_respond(
|
|||
let mut sig_status = String::from("SignedPayload: not present");
|
||||
let mut secure_status = String::from("SecurePayload: not present");
|
||||
|
||||
let enc = msg.get_data(DataType::EncryptedPayload);
|
||||
if matches!(enc, DataValue::EncryptedContainer(_)) {
|
||||
let mut dv = enc.clone();
|
||||
if dv
|
||||
.decrypt_into_container(host_keyring, b"demo-aad")
|
||||
.is_some()
|
||||
{
|
||||
if let Some(enc @ DataValue::Encrypted(_)) = msg.get_data(DataType::EncryptedPayload) {
|
||||
if let Ok(dv) = enc.decrypt(host_keyring, mtp::codec::ProtectionPurpose::from(1)) {
|
||||
if let Some(entries) = dv.as_container() {
|
||||
println!(" Decrypted EncryptedPayload: {:?}", entries);
|
||||
enc_status = format!("EncryptedPayload decrypted OK ({} entries)", entries.len());
|
||||
|
|
@ -99,13 +300,19 @@ pub fn process_and_respond(
|
|||
}
|
||||
}
|
||||
|
||||
let sig = msg.get_data(DataType::SignedPayload);
|
||||
if matches!(sig, DataValue::SignedContainer(_)) {
|
||||
if let Some(sig @ DataValue::Signed(_)) = msg.get_data(DataType::SignedPayload) {
|
||||
if let Some(pk_bundle) = client_pk {
|
||||
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
||||
let mut dv = sig.clone();
|
||||
if dv.verify_into_container(&verifier).is_some() {
|
||||
if let Some(entries) = dv.as_container() {
|
||||
let signer_id = sig.as_signed().map(|signed| signed.signer_id);
|
||||
if let Some(signer_id) = signer_id
|
||||
&& sig
|
||||
.verify(signer_id, pk_bundle, mtp::codec::ProtectionPurpose::from(2))
|
||||
.is_ok()
|
||||
{
|
||||
let dv = sig
|
||||
.clone()
|
||||
.into_verified(signer_id, pk_bundle, mtp::codec::ProtectionPurpose::from(2))
|
||||
.ok();
|
||||
if let Some(entries) = dv.and_then(|value| value.as_container()) {
|
||||
println!(" Verified SignedPayload: {:?}", entries);
|
||||
sig_status = format!("SignedPayload verified OK ({} entries)", entries.len());
|
||||
}
|
||||
|
|
@ -119,17 +326,23 @@ pub fn process_and_respond(
|
|||
}
|
||||
}
|
||||
|
||||
let secure = msg.get_data(DataType::SecurePayload);
|
||||
if matches!(secure, DataValue::SignedEncryptedContainer(_)) {
|
||||
if let Some(secure @ DataValue::Encrypted(_)) = msg.get_data(DataType::SecurePayload) {
|
||||
if let Some(pk_bundle) = client_pk {
|
||||
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
||||
let mut dv = secure.clone();
|
||||
if dv
|
||||
.decrypt_signed_encrypted_container(host_keyring, b"demo-aad")
|
||||
.is_some()
|
||||
&& dv.verify_into_container(&verifier).is_some()
|
||||
if let Ok(opened) = secure.decrypt(host_keyring, mtp::codec::ProtectionPurpose::from(4))
|
||||
&& let Some(signed) = opened.as_signed()
|
||||
&& opened
|
||||
.verify(
|
||||
signed.signer_id,
|
||||
pk_bundle,
|
||||
mtp::codec::ProtectionPurpose::from(3),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if let Some(entries) = dv.as_container() {
|
||||
let signer_id = signed.signer_id;
|
||||
let dv = opened
|
||||
.into_verified(signer_id, pk_bundle, mtp::codec::ProtectionPurpose::from(3))
|
||||
.ok();
|
||||
if let Some(entries) = dv.and_then(|value| value.as_container()) {
|
||||
println!(" Verified SecurePayload: {:?}", entries);
|
||||
secure_status = format!(
|
||||
"SecurePayload decrypted+verified OK ({} entries)",
|
||||
|
|
@ -149,11 +362,13 @@ pub fn process_and_respond(
|
|||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| e.to_string())?
|
||||
.as_secs();
|
||||
.as_millis();
|
||||
|
||||
Ok(CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||
.add_data(desc_id, description.clone())
|
||||
let response = CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||
.add_data(desc_id, description)
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(ts_id, DataValue::UnsignedNumber(now as u128))
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(
|
||||
data_id,
|
||||
DataValue::Str(format!(
|
||||
|
|
@ -161,8 +376,14 @@ pub fn process_and_respond(
|
|||
enc_status, sig_status, secure_status
|
||||
)),
|
||||
)
|
||||
.add_data(flags_id, flags.clone())
|
||||
.add_data(value_id, value.clone())
|
||||
.add_data(bin_id, binary.clone())
|
||||
.add_data(items_id, items.clone()))
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(flags_id, flags)
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(value_id, value)
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(bin_id, binary)
|
||||
.map_err(|e| e.to_string())?
|
||||
.add_data(items_id, items)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ mod tls;
|
|||
#[path = "web-server.rs"]
|
||||
mod web_server;
|
||||
|
||||
use mtp::host::HostConfig;
|
||||
use mtp::host::{AuthenticationPolicy, AuthState, HostConfig};
|
||||
use mtp::type_map::TypeMap;
|
||||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
|
|
@ -136,7 +136,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
host_keyring,
|
||||
Box::new(get_existing_client),
|
||||
Box::new(complete_register),
|
||||
);
|
||||
)
|
||||
.with_authentication_policy(AuthenticationPolicy::AllowAuthentication);
|
||||
|
||||
let mut host = mtp::webserver::MTPWebServer::new(config, web_server::config()?).await?;
|
||||
println!("Server listening on https://{}", host.local_addr());
|
||||
|
|
@ -158,9 +159,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
};
|
||||
let decrypt_keyring = Arc::clone(&decrypt_keyring);
|
||||
let metrics = Arc::clone(&metrics);
|
||||
let registered_clients = Arc::clone(&clients);
|
||||
metrics.record_connection_version(&conn.version.to_string());
|
||||
tokio::spawn(async move {
|
||||
let desc = conn.description.as_deref().unwrap_or("(no description)");
|
||||
let connection_state = match &conn.auth_state {
|
||||
AuthState::Authenticated => "authenticated client",
|
||||
AuthState::Unauthenticated => "unauthenticated client",
|
||||
AuthState::Pending => "pending client",
|
||||
AuthState::Failed => "failed client",
|
||||
};
|
||||
println!(
|
||||
"\n--- New connection (version {}, remote: {}, description: {desc}) ---",
|
||||
conn.version,
|
||||
|
|
@ -168,7 +176,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.map(|addr| addr.to_string())
|
||||
.unwrap_or_else(|| "unknown".into())
|
||||
);
|
||||
println!("Client ID: {}", conn.client_id);
|
||||
println!("Connection state: {connection_state}; MTP ID: {}", conn.client_id);
|
||||
|
||||
let mut session = metrics.start_session(conn.client_id, desc.to_string());
|
||||
|
||||
|
|
@ -177,6 +185,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
println!("Waiting for messages / pipe requests ...");
|
||||
let mut pipe_open = true;
|
||||
let mut message_open = true;
|
||||
let mut accepted_direct_messages = mtp::codec::InMemoryReplayGuard::default();
|
||||
let mut accepted_relay_messages = mtp::codec::InMemoryReplayGuard::default();
|
||||
let mut exit_reason = "normal".to_string();
|
||||
|
||||
while pipe_open || message_open {
|
||||
|
|
@ -215,11 +225,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
Ok(message) => {
|
||||
println!("Received: {message}");
|
||||
let msg_start = std::time::Instant::now();
|
||||
let registered_clients = registered_clients
|
||||
.lock()
|
||||
.map(|clients| clients.clone())
|
||||
.unwrap_or_default();
|
||||
let result = handlers::process_and_respond(
|
||||
&message,
|
||||
tm,
|
||||
conn.client_public_key.as_ref(),
|
||||
®istered_clients,
|
||||
&decrypt_keyring,
|
||||
&mut accepted_direct_messages,
|
||||
&mut accepted_relay_messages,
|
||||
);
|
||||
let latency = msg_start.elapsed();
|
||||
let ok = result.is_ok();
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ impl ServerMetrics {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session handle — local accumulators, no mutex contention during connection
|
||||
// Session handle, local accumulators, no mutex contention during connection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct SessionHandle<'a> {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,13 @@
|
|||
protocol_version: "1.0"
|
||||
protocol_version: "3.0"
|
||||
|
||||
type_maps:
|
||||
"0.0":
|
||||
"3.0":
|
||||
CommunicationTypes:
|
||||
ProtectedMessage: 32
|
||||
AlternateMessage: 33
|
||||
DataTypes:
|
||||
"1.0":
|
||||
CommunicationTypes:
|
||||
CommunicationType: 32
|
||||
DataTypes:
|
||||
Data: 32
|
||||
Flags: 33
|
||||
Value: 34
|
||||
BinaryData: 35
|
||||
Items: 36
|
||||
EncryptedPayload: 37
|
||||
SignedPayload: 38
|
||||
SecurePayload: 39
|
||||
CommunicationType: 40
|
||||
DataType: 41
|
||||
"2.0":
|
||||
CommunicationTypes:
|
||||
CommunicationType: 32
|
||||
DataTypes:
|
||||
Data: 34
|
||||
Flags: 33
|
||||
Value: 35
|
||||
BinaryData: 36
|
||||
Items: 37
|
||||
|
|
@ -32,3 +16,7 @@ type_maps:
|
|||
SecurePayload: 40
|
||||
CommunicationType: 41
|
||||
DataType: 42
|
||||
ExampleText: 43
|
||||
ExampleNumber: 44
|
||||
ExampleRole: 45
|
||||
ExampleMetadata: 46
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@
|
|||
Use new credentials
|
||||
</button>
|
||||
<button id="connect" type="button" disabled>Connect</button>
|
||||
<button id="connect-unauthenticated" type="button" disabled>
|
||||
Connect Unauthenticated
|
||||
</button>
|
||||
<button id="clear-keys" type="button">Clear saved keys</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ const GENERATE_KEYPAIR = document.getElementById(
|
|||
"generate-keypair",
|
||||
) as HTMLButtonElement;
|
||||
const CONNECT = document.getElementById("connect") as HTMLButtonElement;
|
||||
const CONNECT_UNAUTHENTICATED = document.getElementById(
|
||||
"connect-unauthenticated",
|
||||
) as HTMLButtonElement;
|
||||
const CLEAR_KEYS = document.getElementById("clear-keys") as HTMLButtonElement;
|
||||
const STREAM_MIC = document.getElementById("stream-mic") as HTMLButtonElement;
|
||||
const STOP_MIC = document.getElementById("stop-mic") as HTMLButtonElement;
|
||||
|
|
@ -32,7 +35,6 @@ const HOST_PUBLIC_KEY_KEY = "mtp-web-client-host-public-key";
|
|||
type SavedKeys = {
|
||||
clientId: string | null;
|
||||
keyring?: number[];
|
||||
keyringBytes?: number[];
|
||||
hostPublicKey?: number[];
|
||||
};
|
||||
|
||||
|
|
@ -290,7 +292,7 @@ function loadKeys() {
|
|||
|
||||
const data = JSON.parse(raw) as SavedKeys;
|
||||
clientId = data.clientId ? BigInt(data.clientId) : null;
|
||||
const keyringLength = (data.keyring ?? data.keyringBytes ?? []).length;
|
||||
const keyringLength = (data.keyring ?? []).length;
|
||||
CLIENT_CREDENTIALS.value = renderStructured({
|
||||
clientId: data.clientId,
|
||||
keyringBytes: keyringLength,
|
||||
|
|
@ -352,6 +354,7 @@ async function initWasm() {
|
|||
const supported = MTPClient.isSupported();
|
||||
log(`WASM loaded. WebTransport supported: ${supported}`);
|
||||
CONNECT.disabled = !supported;
|
||||
CONNECT_UNAUTHENTICATED.disabled = !supported;
|
||||
}
|
||||
|
||||
async function createClient() {
|
||||
|
|
@ -448,6 +451,39 @@ async function connect() {
|
|||
}
|
||||
}
|
||||
|
||||
async function connectUnauthenticated() {
|
||||
STATUS.textContent = "";
|
||||
PIPE_STATUS.textContent = "";
|
||||
|
||||
if (!MTPClient.isSupported()) {
|
||||
log("WebTransport is not supported in this browser.", "error");
|
||||
return;
|
||||
}
|
||||
saveHostPublicKey();
|
||||
|
||||
try {
|
||||
const client = await createClient();
|
||||
activeClient = client;
|
||||
const storedIdentity = client.credentials?.clientId;
|
||||
await client.connectUnauthenticated();
|
||||
|
||||
clientId = storedIdentity ?? clientId;
|
||||
loadKeys();
|
||||
log(
|
||||
`Connected over an unauthenticated transport (guest connection). Stored protection identity ${storedIdentity == null ? "not registered" : `${storedIdentity} retained`}.`,
|
||||
);
|
||||
log(
|
||||
"The explicit connectUnauthenticated() path did not delete or replace stored credentials.",
|
||||
"state",
|
||||
);
|
||||
STREAM_MIC.disabled = false;
|
||||
log("\nPipe demo ready. Click 'Stream Microphone' to start.", "pipe");
|
||||
updateMetrics();
|
||||
} catch (error) {
|
||||
log(`[error] ${error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function startMicStreaming() {
|
||||
if (!activeClient) {
|
||||
pipeLog("No active client connection.", "error");
|
||||
|
|
@ -719,6 +755,13 @@ CONNECT.addEventListener("click", () => {
|
|||
});
|
||||
});
|
||||
|
||||
CONNECT_UNAUTHENTICATED.addEventListener("click", () => {
|
||||
connectUnauthenticated().catch((e) => {
|
||||
log(`Unauthenticated connection failed: ${e}`, "error");
|
||||
console.error(e);
|
||||
});
|
||||
});
|
||||
|
||||
CLEAR_KEYS.addEventListener("click", () => {
|
||||
clientId = null;
|
||||
CLIENT_CREDENTIALS.value = "";
|
||||
|
|
|
|||
Loading…
Reference in a new issue