[WIP] Security work While on holiday

This commit is contained in:
Alex 2026-08-12 22:45:28 +02:00
commit 7f0231e3f1
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
109 changed files with 19694 additions and 5210 deletions

View file

@ -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)
}

View file

@ -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 {

View file

@ -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) => {

View file

@ -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
};

View 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)
}