diff --git a/rust-zerotier-core/src/certificate.rs b/rust-zerotier-core/src/certificate.rs index a335c43ff..9d930098c 100644 --- a/rust-zerotier-core/src/certificate.rs +++ b/rust-zerotier-core/src/certificate.rs @@ -320,28 +320,45 @@ implement_to_from_json!(CertificateName); #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct CertificateNetwork { pub id: NetworkId, - pub controller: Fingerprint, + pub controller: Option, } impl CertificateNetwork { pub(crate) fn new_from_capi(cn: &ztcore::ZT_Certificate_Network) -> CertificateNetwork { - CertificateNetwork { - id: NetworkId(cn.id), - controller: Fingerprint { - address: Address(cn.controller.address), - hash: cn.controller.hash, - }, + if is_all_zeroes(cn.controller.hash) { + CertificateNetwork { + id: NetworkId(cn.id), + controller: None, + } + } else { + CertificateNetwork { + id: NetworkId(cn.id), + controller: Some(Fingerprint { + address: Address(cn.controller.address), + hash: cn.controller.hash, + }), + } } } pub(crate) fn to_capi(&self) -> ztcore::ZT_Certificate_Network { - ztcore::ZT_Certificate_Network { - id: self.id.0, - controller: ztcore::ZT_Fingerprint { - address: self.controller.address.0, - hash: self.controller.hash, - }, - } + self.controller.as_ref().map_or_else(|| { + ztcore::ZT_Certificate_Network { + id: self.id.0, + controller: ztcore::ZT_Fingerprint { + address: 0, + hash: [0_u8; 48], + } + } + }, |controller| { + ztcore::ZT_Certificate_Network { + id: self.id.0, + controller: ztcore::ZT_Fingerprint { + address: controller.address.0, + hash: controller.hash, + }, + } + }) } } @@ -533,10 +550,10 @@ impl CertificateSubject { } } - pub fn new_csr(&self, uid: Option<&CertificateSubjectUniqueIdSecret>) -> Result, ResultCode> { + pub fn new_csr(&self, uid: Option<&CertificateSubjectUniqueIdSecret>) -> Result, ResultCode> { let mut csr: Vec = Vec::new(); - csr.resize(16384, 0); - let mut csr_size: c_int = 16384; + csr.resize(65536, 0); + let mut csr_size: c_int = 65536; unsafe { let capi = self.to_capi(); @@ -551,8 +568,9 @@ impl CertificateSubject { } } } + csr.resize(csr_size as usize, 0); - return Ok(csr.into_boxed_slice()); + return Ok(csr); } } diff --git a/rust-zerotier-core/src/lib.rs b/rust-zerotier-core/src/lib.rs index b58516f76..39227f17c 100644 --- a/rust-zerotier-core/src/lib.rs +++ b/rust-zerotier-core/src/lib.rs @@ -175,6 +175,17 @@ pub fn random() -> u64 { } } +/// Test whether this byte array or slice is all zeroes. +pub fn is_all_zeroes>(b: B) -> bool { + let bb = b.as_ref(); + for c in bb.iter() { + if *c != 0 { + return false; + } + } + true +} + /// The CStr stuff is cumbersome, so this is an easier to use function to turn a C string into a String. /// This returns an empty string on a null pointer or invalid UTF-8. It's unsafe because it can crash if /// the string is not zero-terminated. A size limit can be passed in if available to reduce this risk, or diff --git a/rust-zerotier-service/src/cli.rs b/rust-zerotier-service/src/cli.rs index 76c2e79ea..2c10911a5 100644 --- a/rust-zerotier-service/src/cli.rs +++ b/rust-zerotier-service/src/cli.rs @@ -93,7 +93,7 @@ Advanced Operations: · list List certificates at local node · show Show certificate details newsid [sid secret out] Create a new subject unique ID - newcsr [csr out] Create a subject CSR + newcsr Create a subject CSR (interactive) sign [cert out] Sign a CSR to create a certificate verify Verify certificate (not chain) dump Verify and print certificate @@ -233,7 +233,7 @@ pub(crate) fn parse_cli_args() -> ArgMatches<'static> { .subcommand(App::new("newsid") .arg(Arg::with_name("path").index(1).required(false))) .subcommand(App::new("newcsr") - .arg(Arg::with_name("path").index(2).required(false))) + .arg(Arg::with_name("path").index(1).required(true))) .subcommand(App::new("sign") .arg(Arg::with_name("csr").index(1).required(true)) .arg(Arg::with_name("identity").index(2).required(true)) diff --git a/rust-zerotier-service/src/commands/cert.rs b/rust-zerotier-service/src/commands/cert.rs index fbaaa2783..1f938d533 100644 --- a/rust-zerotier-service/src/commands/cert.rs +++ b/rust-zerotier-service/src/commands/cert.rs @@ -11,22 +11,27 @@ */ /****/ -use clap::ArgMatches; -use crate::store::Store; -use zerotier_core::{CertificateSubjectUniqueIdSecret, CertificateUniqueIdType}; use std::io::Write; +use std::str::FromStr; + +use clap::ArgMatches; +use dialoguer::Input; +use lazy_static::lazy_static; + +use zerotier_core::*; + +use crate::store::Store; +use crate::utils::read_identity; +use futures::SinkExt; -#[inline(always)] fn list(store: &Store, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn show<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn newsid<'a>(store: &Store, cli_args: Option<&ArgMatches<'a>>, auth_token: &Option) -> i32 { let sid = CertificateSubjectUniqueIdSecret::new(CertificateUniqueIdType::NistP384); // right now there's only one type let sid = sid.to_json(); @@ -44,42 +49,225 @@ fn newsid<'a>(store: &Store, cli_args: Option<&ArgMatches<'a>>, auth_token: &Opt } } -#[inline(always)] -fn newcsr<'a>(store: &Store, cli_args: Option<&ArgMatches<'a>>, auth_token: &Option) -> i32 { - 0 +fn newcsr<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { + let theme = &dialoguer::theme::SimpleTheme; + + let subject_unique_id: String = Input::with_theme(theme) + .with_prompt("Path to subject unique ID secret key (recommended)") + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + let subject_unique_id: Option = if subject_unique_id.is_empty() { + None + } else { + let b = crate::utils::read_limit(subject_unique_id, 16384); + if b.is_err() { + println!("ERROR: unable to read subject unique ID secret file: {}", b.err().unwrap().to_string()); + return 1; + } + let json = String::from_utf8(b.unwrap()); + if json.is_err() { + println!("ERROR: invalid subject unique ID secret: {}", json.err().unwrap().to_string()); + return 1; + } + let sid = CertificateSubjectUniqueIdSecret::new_from_json(json.unwrap().as_str()); + if sid.is_err() { + println!("ERROR: invalid subject unique ID secret: {}", sid.err().unwrap()); + return 1; + } + Some(sid.unwrap()) + }; + + let timestamp: i64 = Input::with_theme(theme) + .with_prompt("Subject timestamp (seconds since epoch)") + .with_initial_text((crate::utils::ms_since_epoch() / 1000).to_string()) + .allow_empty(false) + .interact_text() + .unwrap_or(0); + if timestamp < 0 { + println!("ERROR: invalid timestamp"); + return 1; + } + + println!("Identities to include in subject"); + let mut identities: Vec = Vec::new(); + loop { + let identity: String = Input::with_theme(theme) + .with_prompt(format!(" [{}] Identity or path to identity (empty to end)", identities.len() + 1)) + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + if identity.is_empty() { + break; + } + let identity = read_identity(identity.as_str(), true); + if identity.is_err() { + println!("ERROR: identity invalid or unable to read from file."); + return 1; + } + let identity = identity.unwrap(); + if identity.has_private() { + println!("ERROR: identity contains private key, use public only for CSR!"); + return 1; + } + + let locator: String = Input::with_theme(theme) + .with_prompt(format!(" [{}] Locator for {} (optional)", identities.len() + 1, identity.address.to_string())) + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + let locator = if locator.is_empty() { + None + } else { + let l = Locator::new_from_string(locator.as_str()); + if l.is_err() { + println!("ERROR: locator invalid: {}", l.err().unwrap().to_str()); + return 1; + } + l.ok() + }; + + identities.push(CertificateIdentity { + identity, + locator, + }); + } + + println!("Networks to include in subject (empty to end)"); + let mut networks: Vec = Vec::new(); + loop { + let nwid: String = Input::with_theme(theme) + .with_prompt(format!(" [{}] Network ID (empty to end)", networks.len() + 1)) + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + if nwid.len() != 16 { + break; + } + let nwid = NetworkId::new_from_string(nwid.as_str()); + + let fingerprint: String = Input::with_theme(theme) + .with_prompt(format!(" [{}] Fingerprint of primary controller (optional)", networks.len() + 1)) + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + let fingerprint = if fingerprint.is_empty() { + None + } else { + let f = Fingerprint::new_from_string(fingerprint.as_str()); + if f.is_err() { + println!("ERROR: fingerprint invalid: {}", f.err().unwrap().to_str()); + return 1; + } + f.ok() + }; + + networks.push(CertificateNetwork { + id: nwid, + controller: fingerprint, + }) + } + + println!("Certificates to reference in subject (empty to end)"); + let mut certificates: Vec = Vec::new(); + loop { + let sn: String = Input::with_theme(theme) + .with_prompt(format!(" [{}] Certificate serial number (empty to end)", certificates.len() + 1)) + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + if sn.is_empty() { + break; + } + let sn = CertificateSerialNo::new_from_string(sn.as_str()); + if sn.is_err() { + println!("ERROR: invalid certificate serial number: {}", sn.err().unwrap().to_str()); + return 1; + } + certificates.push(sn.ok().unwrap()); + } + + println!("URLs to check for updated certificates for this subject"); + let mut update_urls: Vec = Vec::new(); + loop { + let url: String = Input::with_theme(theme) + .with_prompt(format!(" [{}] URL (empty to end)", update_urls.len() + 1)) + .allow_empty(true) + .interact_text() + .unwrap_or_default(); + if url.is_empty() { + break; + } + let url_parsed = hyper::Uri::from_str(url.as_str()); + if url_parsed.is_err() { + println!("ERROR: invalid URL: {}", url_parsed.err().unwrap().to_string()); + return 1; + } + update_urls.push(url); + } + + println!("Certificate \"name\" (same as X509 certificates, all fields optional)"); + let name = CertificateName { + serial_no: Input::with_theme(theme).with_prompt(" Serial").allow_empty(true).interact_text().unwrap_or_default(), + common_name: Input::with_theme(theme).with_prompt(" Common Name").allow_empty(true).interact_text().unwrap_or_default(), + organization: Input::with_theme(theme).with_prompt(" Organization").allow_empty(true).interact_text().unwrap_or_default(), + unit: Input::with_theme(theme).with_prompt(" Organizational Unit").allow_empty(true).interact_text().unwrap_or_default(), + country: Input::with_theme(theme).with_prompt(" Country").allow_empty(true).interact_text().unwrap_or_default(), + province: Input::with_theme(theme).with_prompt(" State/Province").allow_empty(true).interact_text().unwrap_or_default(), + locality: Input::with_theme(theme).with_prompt(" Locality").allow_empty(true).interact_text().unwrap_or_default(), + street_address: Input::with_theme(theme).with_prompt(" Street Address").allow_empty(true).interact_text().unwrap_or_default(), + postal_code: Input::with_theme(theme).with_prompt(" Postal Code").allow_empty(true).interact_text().unwrap_or_default(), + email: Input::with_theme(theme).with_prompt(" E-Mail").allow_empty(true).interact_text().unwrap_or_default(), + url: Input::with_theme(theme).with_prompt(" URL (informational)").allow_empty(true).interact_text().unwrap_or_default(), + host: Input::with_theme(theme).with_prompt(" Host").allow_empty(true).interact_text().unwrap_or_default(), + }; + + let subject = CertificateSubject { + timestamp, + identities, + networks, + certificates, + update_urls, + name, + unique_id: Vec::new(), + unique_id_proof_signature: Vec::new(), + }; + subject.new_csr(subject_unique_id.as_ref()).map_or(1, |csr| { + let p = cli_args.value_of("path").unwrap(); + std::fs::write(p, csr).map_or_else(|e| { + println!("ERROR: unable to write CSR: {}", e.to_string()); + 1 + }, |_| { + println!("CSR written to {}", p); + 0 + }) + }) } -#[inline(always)] fn sign<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn verify<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn dump<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn import<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn restore(store: &Store, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn export<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } -#[inline(always)] fn delete<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option) -> i32 { 0 } @@ -89,7 +277,7 @@ pub(crate) fn run<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Opt ("list", None) => list(store, auth_token), ("show", Some(sub_cli_args)) => show(store, sub_cli_args, auth_token), ("newsid", sub_cli_args) => newsid(store, sub_cli_args, auth_token), - ("newcsr", sub_cli_args) => newcsr(store, sub_cli_args, auth_token), + ("newcsr", Some(sub_cli_args)) => newcsr(store, sub_cli_args, auth_token), ("sign", Some(sub_cli_args)) => sign(store, sub_cli_args, auth_token), ("verify", Some(sub_cli_args)) => verify(store, sub_cli_args, auth_token), ("dump", Some(sub_cli_args)) => dump(store, sub_cli_args, auth_token), diff --git a/rust-zerotier-service/src/utils.rs b/rust-zerotier-service/src/utils.rs index ae58f16be..08272937d 100644 --- a/rust-zerotier-service/src/utils.rs +++ b/rust-zerotier-service/src/utils.rs @@ -11,8 +11,14 @@ */ /****/ +use std::error::Error; +use std::fs::File; +use std::io::Read; use std::mem::MaybeUninit; use std::os::raw::c_uint; +use std::path::Path; + +use zerotier_core::Identity; use crate::osdep; @@ -41,3 +47,48 @@ pub(crate) fn ms_since_epoch() -> i64 { // This is easy to do in the Rust stdlib, but the version in OSUtils is probably faster. unsafe { osdep::msSinceEpoch() } } + +/// Convenience function to read up to limit bytes from a file. +/// If the file is larger than limit, the excess is not read. +pub(crate) fn read_limit>(path: P, limit: usize) -> std::io::Result> { + let mut v: Vec = Vec::new(); + let _ = File::open(path)?.take(limit as u64).read_to_end(&mut v)?; + Ok(v) +} + +/// Read an identity as either a literal or from a file. +/// This is used in parsing command lines, allowing either a literal or a path +/// to be specified and automagically disambiguating. +pub(crate) fn read_identity(input: &str, validate: bool) -> Result { + let id = Identity::new_from_string(input); + if id.is_err() { + let input = Path::new(input); + if !input.exists() || !input.is_file() { + return Err(format!("invalid identity: {}", id.err().unwrap().to_str())); + } + read_limit(input, 16384).map_or_else(|e| { + Err(e.to_string()) + }, |v| { + String::from_utf8(v).map_or_else(|e| { + Err(e.to_string()) + }, |s| { + Identity::new_from_string(s.as_str()).map_or_else(|_| { + Err(format!("Invalid identity in file {}", input.to_str().unwrap_or(""))) + }, |id| { + if validate && !id.validate() { + Err(String::from("invalid identity: local validation failed")) + } else { + Ok(id) + } + }) + }) + }) + } else { + let id = id.ok().unwrap(); + if validate && !id.validate() { + Err(String::from("invalid identity: local validation failed")) + } else { + Ok(id) + } + } +} diff --git a/rust-zerotier-service/src/vnic/mac_feth_tap.rs b/rust-zerotier-service/src/vnic/mac_feth_tap.rs index e064ece39..d425b7d82 100644 --- a/rust-zerotier-service/src/vnic/mac_feth_tap.rs +++ b/rust-zerotier-service/src/vnic/mac_feth_tap.rs @@ -57,8 +57,8 @@ use crate::vnic::VNIC; use crate::osdep::getifmaddrs; const BPF_BUFFER_SIZE: usize = 131072; -const IFCONFIG: &str = "/sbin/ifconfig"; -const SYSCTL: &str = "/usr/sbin/sysctl"; +const IFCONFIG: &'static str = "/sbin/ifconfig"; +const SYSCTL: &'static str = "/usr/sbin/sysctl"; // Holds names of feth devices and destroys them on Drop. struct MacFethDevice {