//! Integration tests for [`aura_cli::init::provision_client`]. //! //! These tests first generate a CA + server cert via `pki::init` / `pki::issue_server`, then //! drive `provision_client` against that CA and verify: //! //! * the bundle directory ends up with `ca.crt`, `client.crt`, `client.key`, `client.toml`; //! * the rendered `client.toml` parses; //! * the issued client cert verifies against the original CA via [`AuraCertVerifier`]; //! * `--id` defaults to a UUID v4 and is reflected as the cert CN. use std::path::PathBuf; use aura_cli::config::ClientConfigFile; use aura_cli::init::{self, ProvisionClientOpts}; use aura_cli::pki; use aura_pki::AuraCertVerifier; use rustls_pki_types::CertificateDer; /// Per-test temp dir. fn temp_dir(tag: &str) -> PathBuf { let mut dir = std::env::temp_dir(); dir.push(format!( "aura-cli-provision-{tag}-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&dir).expect("create temp dir"); dir } /// Generate a CA at `ca_dir` for the rest of the test to use. fn bootstrap_ca(ca_dir: &std::path::Path) { pki::init("Aura Provision Test CA", ca_dir).expect("ca init"); } /// Decode a single-cert PEM into a DER chain for the verifier. fn pem_chain(pem_path: &std::path::Path) -> Vec> { let pem = std::fs::read(pem_path).expect("read cert"); let (_, parsed) = x509_parser::pem::parse_x509_pem(&pem).expect("parse PEM"); vec![CertificateDer::from(parsed.contents)] } /// Extract the certificate's CN via `x509-parser` so we can check that the assigned id ended up /// in the cert. fn cert_common_name(pem_path: &std::path::Path) -> String { let pem = std::fs::read(pem_path).expect("read cert"); let (_, parsed) = x509_parser::pem::parse_x509_pem(&pem).expect("parse PEM"); let (_, cert) = x509_parser::parse_x509_certificate(&parsed.contents).expect("parse cert"); let subject = cert.subject(); for cn in subject.iter_common_name() { if let Ok(s) = cn.as_str() { return s.to_string(); } } panic!("no CN in subject {subject:?}"); } /// Happy path: explicit id, bundle materialises and parses, cert verifies against CA. #[test] fn provision_client_with_explicit_id() { let root = temp_dir("happy"); let ca_dir = root.join("ca"); bootstrap_ca(&ca_dir); let bundle = root.join("client-bundle"); let mut opts = ProvisionClientOpts::new( &ca_dir, "203.0.113.10", "vpn.example.com", "10.7.0.2", &bundle, ); opts.id = Some("phone-1".to_string()); let report = init::provision_client(&opts).expect("provision"); assert_eq!(report.id, "phone-1", "explicit id preserved"); assert!(report.ca_cert.exists()); assert!(report.client_cert.exists()); assert!(report.client_key.exists()); assert!(report.client_config.exists()); // The bundled cert's CN matches the id we passed. assert_eq!(cert_common_name(&report.client_cert), "phone-1"); // The client.toml round-trips through the parser cleanly. let cfg = ClientConfigFile::load(&report.client_config).expect("parse client.toml"); assert_eq!(cfg.client.server_addr, "203.0.113.10:443"); assert_eq!(cfg.client.sni, "vpn.example.com"); assert_eq!(cfg.tunnel.local_ip, "10.7.0.2"); assert!(cfg.client.bridges.is_empty(), "no bridges by default"); // The verifier accepts the bundled chain against the same CA we issued from. let ca_pem = std::fs::read_to_string(ca_dir.join(pki::CA_CERT)).expect("read ca"); let verifier = AuraCertVerifier::new(&ca_pem).expect("verifier"); let chain = pem_chain(&report.client_cert); let cn = verifier .verify_client_cert(&chain) .expect("issued client cert chains to the CA"); assert_eq!(cn, "phone-1"); let _ = std::fs::remove_dir_all(&root); } /// Default `--id` path: a fresh UUID v4 is assigned and ends up as the CN. #[test] fn provision_client_default_id_is_uuid_v4() { let root = temp_dir("uuid"); let ca_dir = root.join("ca"); bootstrap_ca(&ca_dir); let bundle = root.join("bundle"); let opts = ProvisionClientOpts::new( &ca_dir, "203.0.113.10", "vpn.example.com", "10.7.0.5", &bundle, ); let report = init::provision_client(&opts).expect("provision"); // The id is a valid UUID v4 and equals the cert CN. let parsed = uuid::Uuid::parse_str(&report.id).expect("id is uuid"); assert_eq!(parsed.get_version_num(), 4, "uuid v4"); assert_eq!(cert_common_name(&report.client_cert), report.id); let _ = std::fs::remove_dir_all(&root); } /// `bridges = [...]` ends up in the rendered client.toml and parses back through the config. #[test] fn provision_client_writes_bridges() { let root = temp_dir("bridges"); let ca_dir = root.join("ca"); bootstrap_ca(&ca_dir); let bundle = root.join("bundle"); let mut opts = ProvisionClientOpts::new( &ca_dir, "203.0.113.10", "vpn.example.com", "10.7.0.3", &bundle, ); opts.bridges = vec!["203.0.113.11".to_string(), "203.0.113.12".to_string()]; let report = init::provision_client(&opts).expect("provision"); let cfg = ClientConfigFile::load(&report.client_config).expect("parse"); assert_eq!(cfg.client.bridges.len(), 2); assert!(cfg.client.bridges.contains(&"203.0.113.11".to_string())); assert!(cfg.client.bridges.contains(&"203.0.113.12".to_string())); let _ = std::fs::remove_dir_all(&root); } /// `enable_knock` / `enable_cover_traffic` flip the rendered TOML's `[transport.knock]` / /// `[transport.cover]` sections. #[test] fn provision_client_anti_surveillance_toggles() { let root = temp_dir("knock"); let ca_dir = root.join("ca"); bootstrap_ca(&ca_dir); let bundle = root.join("bundle"); let mut opts = ProvisionClientOpts::new( &ca_dir, "203.0.113.10", "vpn.example.com", "10.7.0.4", &bundle, ); opts.enable_knock = true; opts.enable_cover_traffic = true; let report = init::provision_client(&opts).expect("provision"); let cfg = ClientConfigFile::load(&report.client_config).expect("parse"); assert!(cfg.transport.knock.enabled); assert!(cfg.transport.cover.enabled); let _ = std::fs::remove_dir_all(&root); } /// A non-empty bundle directory triggers an error without `--force`. #[test] fn provision_client_refuses_non_empty_bundle() { let root = temp_dir("nonempty"); let ca_dir = root.join("ca"); bootstrap_ca(&ca_dir); let bundle = root.join("bundle"); std::fs::create_dir_all(&bundle).unwrap(); std::fs::write(bundle.join("junk.txt"), b"hi").unwrap(); let opts = ProvisionClientOpts::new( &ca_dir, "203.0.113.10", "vpn.example.com", "10.7.0.6", &bundle, ); let err = init::provision_client(&opts).unwrap_err().to_string(); assert!(err.contains("not empty"), "got: {err}"); let _ = std::fs::remove_dir_all(&root); }