Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion openscreen-application/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ uuid = { version = "1.10", features = ["v4"], optional = true }
base64 = { version = "0.22", optional = true }
x509-parser = { version = "0.16", optional = true }
hex = { version = "0.4", optional = true }
hostname = { version = "0.4", optional = true }

[[bin]]
name = "app-sender"
Expand All @@ -42,7 +43,24 @@ required-features = ["bin"]

[features]
default = ["bin"]
bin = ["clap", "tokio", "anyhow", "colored", "tracing", "tracing-subscriber", "quinn", "rcgen", "pem", "uuid", "base64", "x509-parser", "hex", "openscreen-discovery", "openscreen-discovery-mdns"]
bin = [
"anyhow",
"base64",
"clap",
"colored",
"hex",
"hostname",
"openscreen-discovery-mdns",
"openscreen-discovery",
"pem",
"quinn",
"rcgen",
"tokio",
"tracing-subscriber",
"tracing",
"uuid",
"x509-parser",
]

[dev-dependencies]
tempfile = "3.8"
Expand Down
19 changes: 7 additions & 12 deletions openscreen-application/src/bin/app-receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,8 @@ async fn main() -> Result<()> {
async fn run_receiver(args: &Args) -> Result<()> {
// Step 1: Load or generate certificate
println!("WAIT: Loading certificate...");
let cert_key =
CertificateKey::load_or_generate(&args.cert_dir, "receiver", "_openscreen._tcp.local")
.context("Failed to load/generate certificate")?;
let cert_key = CertificateKey::load_or_generate(&args.cert_dir, &args.name, "local")
.context("Failed to load/generate certificate")?;

println!("OK: Certificate loaded");
println!(
Expand Down Expand Up @@ -161,19 +160,15 @@ async fn run_receiver(args: &Args) -> Result<()> {

println!("WAIT: Initializing QUIC server...");

// Convert rcgen certificate to Quinn format
let (cert_der, key_der) = (
cert_key.cert.cert.der().to_vec(),
cert_key.cert.key_pair.serialize_der(),
);
// Pass certificate and key to Quinn
let (cert_der, key_der) = (cert_key.cert_der, cert_key.key_der);

// Pass auth token to server for validation
let auth_token_bytes = Some(auth_token.as_str().as_bytes().to_vec());

let server =
QuinnServer::bind_with_cert(bind_addr, &args.psk, cert_der, key_der, auth_token_bytes)
.await
.context("Failed to bind server")?;
let server = QuinnServer::bind(bind_addr, &args.psk, cert_der, key_der, auth_token_bytes)
.await
.context("Failed to bind server")?;

println!("OK: Listening on {bind_addr}");
println!("{}", "Waiting for connections...".bright_cyan());
Expand Down
38 changes: 34 additions & 4 deletions openscreen-application/src/bin/app-sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use openscreen_application::cert::CertificateKey;
use openscreen_application::messages::{AgentInfoRequest, AgentInfoResponse};
use openscreen_crypto_rustcrypto::RustCryptoCryptoProvider;
use openscreen_discovery::{DiscoveryBrowser, ServiceInfo};
Expand Down Expand Up @@ -237,19 +238,48 @@ async fn run_sender(args: &Args) -> Result<()> {
print!("{}", "WAIT: Initializing QUIC client... ".bright_yellow());
std::io::Write::flush(&mut std::io::stdout())?;

// Generate ephemeral client certificate with device hostname
// Per W3C spec: both clients and servers need proper agent certificates with 160-bit serials
let client_instance_name = match hostname::get() {
Ok(s) => s.to_string_lossy().into_owned(),
Err(e) => {
debug!("Failed to get system hostname: {e}, using fallback");
"openscreen-sender".to_string()
}
};

debug!("Using client instance name: {}", client_instance_name);

// Generate ephemeral certificate (not persisted, per-session)
// This is THE agent certificate used in TLS handshake (not regenerated at QUIC layer)
let client_cert = CertificateKey::generate(&client_instance_name, "local")
.context("Failed to generate client certificate")?;

debug!(
"Generated client certificate with hostname: {}",
client_cert.hostname
);

let crypto_provider = RustCryptoCryptoProvider::new();
let bind_addr = "0.0.0.0:0".parse().unwrap();

// Get expected fingerprint from mDNS discovery (for MITM protection)
let expected_fingerprint = *service_info.fingerprint.as_bytes();

let mut client = QuinnClient::new(crypto_provider, bind_addr, expected_fingerprint)
.context("Failed to create Quinn client")?;
// Create client with spec-compliant agent certificate (160-bit serial)
let mut client = QuinnClient::new(
crypto_provider,
bind_addr,
expected_fingerprint,
client_cert.cert_der.clone(),
client_cert.key_der.clone(),
)
.context("Failed to create Quinn client")?;

println!("OK:");
debug!(
"Quinn client initialized with bind address {} and fingerprint verification",
bind_addr
"Quinn client initialized with hostname {} and fingerprint verification",
client_cert.hostname
);
Comment thread
kaidokert marked this conversation as resolved.

// Step 3: Configure PSK and auth token
Expand Down
42 changes: 19 additions & 23 deletions openscreen-application/src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,7 @@ extern crate std;

use anyhow::{Context, Result};
use openscreen_discovery::Fingerprint;
use std::{
format,
path::Path,
string::{String, ToString},
vec,
vec::Vec,
};
use std::{format, path::Path, string::String, vec, vec::Vec};
use uuid::Uuid;

/// OpenScreen certificate serial number (160 bits per W3C spec)
Expand Down Expand Up @@ -166,9 +160,11 @@ fn sanitize_hostname_component(s: &str) -> String {
}

/// Certificate and private key pair
///
/// Stores certificate and key as DER-encoded bytes
pub struct CertificateKey {
pub cert: rcgen::CertifiedKey,
pub cert_der: Vec<u8>,
pub key_der: Vec<u8>,
pub fingerprint: Fingerprint,
pub serial_number: SerialNumber,
pub hostname: String,
Expand All @@ -192,6 +188,7 @@ impl CertificateKey {

// Generate ECDSA key pair
let key_pair = rcgen::KeyPair::generate().context("Failed to generate key pair")?;
let key_der = key_pair.serialize_der();

// Create certificate parameters with hostname as Subject CN
let mut params = rcgen::CertificateParams::new(vec![hostname.clone()])
Expand All @@ -213,13 +210,13 @@ impl CertificateKey {

let cert_der = cert.der().to_vec();

// Calculate fingerprint (SPKI per spec, not full cert - to be fixed in #1)
// Calculate fingerprint (SPKI SHA-256 per W3C spec)
let fingerprint = Fingerprint::from_der_cert(&cert_der)
.map_err(|e| anyhow::anyhow!("Failed to calculate fingerprint: {e:?}"))?;

Ok(Self {
cert: rcgen::CertifiedKey { cert, key_pair },
cert_der,
key_der,
fingerprint,
serial_number,
hostname,
Expand All @@ -241,14 +238,15 @@ impl CertificateKey {
.with_context(|| format!("Failed to read private key from {}", key_path.display()))?;

let key_pair = rcgen::KeyPair::from_pem(&key_pem).context("Failed to parse private key")?;
let key_der = key_pair.serialize_der();

// Parse PEM to get DER bytes
let cert_der = pem::parse(&cert_pem)
.context("Failed to parse certificate PEM")?
.contents()
.to_vec();

// Calculate fingerprint
// Calculate fingerprint (SPKI SHA-256 per W3C spec)
let fingerprint = Fingerprint::from_der_cert(&cert_der)
.map_err(|e| anyhow::anyhow!("Failed to calculate fingerprint: {e:?}"))?;

Expand All @@ -258,18 +256,9 @@ impl CertificateKey {

let hostname = compute_hostname(&serial_number, instance_name, domain);

// Reconstruct certificate params from the existing cert
// Note: We can't perfectly reconstruct all params, but we can create a basic one
let params = rcgen::CertificateParams::new(vec!["loaded-cert".to_string()])
.context("Failed to create certificate params")?;

let cert = params
.self_signed(&key_pair)
.context("Failed to create certificate")?;

Ok(Self {
cert: rcgen::CertifiedKey { cert, key_pair },
cert_der,
key_der,
fingerprint,
serial_number,
hostname,
Expand All @@ -286,8 +275,14 @@ impl CertificateKey {
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");

let cert_pem = self.cert.cert.pem();
let key_pem = self.cert.key_pair.serialize_pem();
// Encode cert_der as PEM
let cert_pem_obj = pem::Pem::new("CERTIFICATE", self.cert_der.clone());
let cert_pem = pem::encode(&cert_pem_obj);

// Encode key_der as PEM
// The key is in PKCS#8 format, which uses the "PRIVATE KEY" tag
let key_pem_obj = pem::Pem::new("PRIVATE KEY", self.key_der.clone());
let key_pem = pem::encode(&key_pem_obj);

std::fs::write(&cert_path, cert_pem)
.with_context(|| format!("Failed to write certificate to {}", cert_path.display()))?;
Expand Down Expand Up @@ -321,6 +316,7 @@ impl CertificateKey {
#[cfg(test)]
mod tests {
use super::*;
use std::string::ToString;

#[test]
fn test_serial_number_format() {
Expand Down
15 changes: 1 addition & 14 deletions openscreen-quinn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@ workspace = true
[lib]
name = "openscreen_quinn"

[[bin]]
name = "openscreen-test"
path = "src/bin/main.rs"

[[bin]]
name = "openscreen-receiver"
path = "src/bin/receiver.rs"

[dependencies]
openscreen-network = { path = "../openscreen-network" }
openscreen-crypto = { path = "../openscreen-crypto" }
Expand All @@ -39,13 +31,8 @@ x509-parser = "0.16"
thiserror = "2.0"
anyhow = "1.0"

# CLI dependencies
clap = { version = "4.5", features = ["derive"] }
clap-verbosity-flag = "2.2"
colored = "2.1"
rpassword = "7.3"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "chrono"] }

[dev-dependencies]
openscreen-discovery = { path = "../openscreen-discovery" }
Loading
Loading