mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-04-26 08:57:26 +02:00
437 lines
16 KiB
Rust
437 lines
16 KiB
Rust
/*
|
|
* Copyright (c)2013-2021 ZeroTier, Inc.
|
|
*
|
|
* Use of this software is governed by the Business Source License included
|
|
* in the LICENSE.TXT file in the project's root directory.
|
|
*
|
|
* Change Date: 2026-01-01
|
|
*
|
|
* On the date above, in accordance with the Business Source License, use
|
|
* of this software will be governed by version 2.0 of the Apache License.
|
|
*/
|
|
/****/
|
|
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
|
|
use clap::ArgMatches;
|
|
use dialoguer::Input;
|
|
use zerotier_core::*;
|
|
|
|
use crate::store::Store;
|
|
use crate::utils::{read_limit, ms_since_epoch, to_json_pretty};
|
|
use crate::GlobalFlags;
|
|
|
|
/// Dump a certificate in human-readable format to stdout.
|
|
fn dump_cert(certificate: &Certificate) {
|
|
let mut subject_identities = String::new();
|
|
let mut subject_networks = String::new();
|
|
let mut subject_update_urls = String::new();
|
|
let mut usage_flags = String::new();
|
|
|
|
fn string_or_dash(s: &str) -> &str {
|
|
if s.is_empty() {
|
|
"-"
|
|
} else {
|
|
s
|
|
}
|
|
}
|
|
|
|
if certificate.subject.identities.is_empty() {
|
|
subject_identities.push_str(": (none)");
|
|
} else {
|
|
for x in certificate.subject.identities.iter() {
|
|
subject_identities.push_str("\n ");
|
|
subject_identities.push_str(x.identity.to_string().as_str());
|
|
if x.locator.is_some() {
|
|
subject_identities.push_str(" ");
|
|
subject_identities.push_str(x.locator.as_ref().unwrap().to_string().as_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if certificate.subject.networks.is_empty() {
|
|
subject_networks.push_str(": (none)");
|
|
} else {
|
|
for x in certificate.subject.networks.iter() {
|
|
subject_networks.push_str("\n ");
|
|
subject_networks.push_str(x.id.to_string().as_str());
|
|
if x.controller.is_some() {
|
|
subject_networks.push_str(" at ");
|
|
subject_networks.push_str(x.controller.as_ref().unwrap().to_string().as_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if certificate.subject.update_urls.is_empty() {
|
|
subject_update_urls.push_str(": (none)");
|
|
} else {
|
|
for x in certificate.subject.update_urls.iter() {
|
|
subject_update_urls.push_str("\n ");
|
|
subject_update_urls.push_str(x.as_ref());
|
|
}
|
|
}
|
|
|
|
if certificate.usage_flags != 0 {
|
|
usage_flags.push_str(" (");
|
|
for f in ALL_CERTIFICATE_USAGE_FLAGS.iter() {
|
|
if (certificate.usage_flags & (*f).0) != 0 {
|
|
if !usage_flags.is_empty() {
|
|
usage_flags.push(',');
|
|
}
|
|
usage_flags.push_str((*f).1);
|
|
}
|
|
}
|
|
usage_flags.push(')');
|
|
}
|
|
|
|
println!(r###"Serial Number (SHA384): {}
|
|
Usage Flags: 0x{:0>8x}{}
|
|
Timestamp: {}
|
|
Validity: {} to {}
|
|
Subject
|
|
Timestamp: {}
|
|
Identities{}
|
|
Networks{}
|
|
Update URLs{}
|
|
Name
|
|
Serial: {}
|
|
Common Name: {}
|
|
Country: {}
|
|
Organization: {}
|
|
Unit: {}
|
|
Locality: {}
|
|
State/Province: {}
|
|
Street Address: {}
|
|
Postal Code: {}
|
|
E-Mail: {}
|
|
URL: {}
|
|
Host: {}
|
|
Unique ID: {}
|
|
Unique ID Signature: {}
|
|
Issuer: {}
|
|
Issuer Public Key: {}
|
|
Public Key: {}
|
|
Extended Attributes: {} bytes
|
|
Signature: {}
|
|
Maximum Path Length: {}{}"###,
|
|
certificate.serial_no.to_string(),
|
|
certificate.usage_flags, usage_flags,
|
|
certificate.timestamp,
|
|
certificate.validity[0], certificate.validity[1],
|
|
certificate.subject.timestamp,
|
|
subject_identities,
|
|
subject_networks,
|
|
subject_update_urls,
|
|
string_or_dash(certificate.subject.name.serial_no.as_str()),
|
|
string_or_dash(certificate.subject.name.common_name.as_str()),
|
|
string_or_dash(certificate.subject.name.country.as_str()),
|
|
string_or_dash(certificate.subject.name.organization.as_str()),
|
|
string_or_dash(certificate.subject.name.unit.as_str()),
|
|
string_or_dash(certificate.subject.name.locality.as_str()),
|
|
string_or_dash(certificate.subject.name.province.as_str()),
|
|
string_or_dash(certificate.subject.name.street_address.as_str()),
|
|
string_or_dash(certificate.subject.name.postal_code.as_str()),
|
|
string_or_dash(certificate.subject.name.email.as_str()),
|
|
string_or_dash(certificate.subject.name.url.as_str()),
|
|
string_or_dash(certificate.subject.name.host.as_str()),
|
|
string_or_dash(base64_encode(&certificate.subject.unique_id).as_str()),
|
|
string_or_dash(base64_encode(&certificate.subject.unique_id_signature).as_str()),
|
|
certificate.issuer.to_string(),
|
|
string_or_dash(base64_encode(&certificate.issuer_public_key).as_str()),
|
|
string_or_dash(base64_encode(&certificate.public_key).as_str()),
|
|
certificate.extended_attributes.len(),
|
|
string_or_dash(base64_encode(&certificate.signature).as_str()),
|
|
certificate.max_path_length, if certificate.max_path_length == 0 { " (leaf)" } else { " (CA or sub-CA)" });
|
|
}
|
|
|
|
fn list(store: &Arc<Store>) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn show<'a>(store: &Arc<Store>, global_flags: &GlobalFlags, cli_args: &ArgMatches<'a>) -> i32 {
|
|
let serial_or_path = cli_args.value_of("serialorpath").unwrap().trim();
|
|
CertificateSerialNo::new_from_string(serial_or_path).map_or_else(|| {
|
|
read_limit(serial_or_path, 65536).map_or_else(|e| {
|
|
println!("ERROR: unable to read certificate from '{}': {}", serial_or_path, e.to_string());
|
|
1
|
|
}, |cert_json| {
|
|
serde_json::from_slice::<Certificate>(cert_json.as_ref()).map_or_else(|e| {
|
|
println!("ERROR: unable to decode certificate from '{}': {}", serial_or_path, e.to_string());
|
|
1
|
|
}, |certificate| {
|
|
if global_flags.json_output {
|
|
println!("{}", to_json_pretty(&certificate));
|
|
} else {
|
|
dump_cert(&certificate);
|
|
}
|
|
let cv = certificate.verify(ms_since_epoch());
|
|
if cv != CertificateError::None {
|
|
println!("\nWARNING: certificate validity check failed: {}", cv.to_str());
|
|
}
|
|
0
|
|
})
|
|
})
|
|
}, |serial| {
|
|
// TODO: query node
|
|
0
|
|
})
|
|
}
|
|
|
|
fn newsuid(cli_args: Option<&ArgMatches>) -> i32 {
|
|
let key_pair = Certificate::new_key_pair(CertificatePublicKeyAlgorithm::ECDSANistP384);
|
|
if key_pair.is_err() {
|
|
println!("ERROR: internal error creating key pair: {}", key_pair.err().unwrap().to_str());
|
|
1
|
|
} else {
|
|
let (_, privk) = key_pair.ok().unwrap();
|
|
let privk_base64 = base64_encode(&privk);
|
|
let path = cli_args.map_or("", |cli_args| { cli_args.value_of("path").unwrap_or("") });
|
|
if path.is_empty() {
|
|
println!("{}", privk_base64);
|
|
0
|
|
} else {
|
|
std::fs::write(path, privk_base64.as_bytes()).map_or_else(|e| {
|
|
eprintln!("FATAL: error writing '{}': {}", path, e.to_string());
|
|
e.raw_os_error().unwrap_or(1)
|
|
}, |_| {
|
|
println!("Subject unique ID secret written to: {} (public is included)", path);
|
|
0
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn newcsr(cli_args: &ArgMatches) -> i32 {
|
|
let theme = &dialoguer::theme::SimpleTheme;
|
|
|
|
let subject_unique_id: String = Input::with_theme(theme)
|
|
.with_prompt("Path to subject unique ID secret key (empty to create unsigned subject)")
|
|
.allow_empty(true)
|
|
.interact_text()
|
|
.unwrap_or_default();
|
|
let subject_unique_id_private_key = if subject_unique_id.is_empty() {
|
|
None
|
|
} else {
|
|
let b = crate::utils::read_limit(subject_unique_id, 1024);
|
|
if b.is_err() {
|
|
println!("ERROR: unable to read subject unique ID secret file: {}", b.err().unwrap().to_string());
|
|
return 1;
|
|
}
|
|
let privk_hex = String::from_utf8(b.unwrap());
|
|
if privk_hex.is_err() {
|
|
println!("ERROR: invalid UTF-8 in secret");
|
|
return 1;
|
|
}
|
|
let privk = hex::decode(privk_hex.unwrap().trim());
|
|
if privk.is_err() || privk.as_ref().unwrap().is_empty() {
|
|
println!("ERROR: invalid unique ID secret: {}", privk.err().unwrap().to_string());
|
|
return 1;
|
|
}
|
|
Some(privk.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!("Subject identities");
|
|
let mut identities: Vec<CertificateIdentity> = 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 = crate::utils::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 or path to 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 = crate::utils::read_locator(locator.as_str());
|
|
if l.is_err() {
|
|
println!("ERROR: locator invalid: {}", l.err().unwrap());
|
|
return 1;
|
|
}
|
|
let l = l.ok();
|
|
if !l.as_ref().unwrap().verify(&identity) {
|
|
println!("ERROR: locator was not signed by this identity.");
|
|
return 1;
|
|
}
|
|
l
|
|
};
|
|
|
|
identities.push(CertificateIdentity {
|
|
identity,
|
|
locator,
|
|
});
|
|
}
|
|
|
|
println!("Subject networks (empty to end)");
|
|
let mut networks: Vec<CertificateNetwork> = 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::from(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!("Subject certificate update URLs");
|
|
let mut update_urls: Vec<String> = 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 information (all fields are 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,
|
|
update_urls,
|
|
name,
|
|
unique_id: Vec::new(),
|
|
unique_id_signature: Vec::new(),
|
|
};
|
|
let (pubk, privk) = Certificate::new_key_pair(CertificatePublicKeyAlgorithm::ECDSANistP384).ok().unwrap();
|
|
subject.new_csr(pubk.as_ref(), subject_unique_id_private_key.as_ref().map(|k| k.as_ref())).map_or_else(|e| {
|
|
println!("ERROR: error creating CRL: {}", e.to_str());
|
|
1
|
|
}, |csr| {
|
|
let csr_path = cli_args.value_of("csrpath").unwrap();
|
|
std::fs::write(csr_path, csr).map_or_else(|e| {
|
|
println!("ERROR: unable to write CSR: {}", e.to_string());
|
|
1
|
|
}, |_| {
|
|
let secret_path = cli_args.value_of("secretpath").unwrap();
|
|
std::fs::write(secret_path, hex::encode(privk)).map_or_else(|e| {
|
|
let _ = std::fs::remove_file(csr_path);
|
|
println!("ERROR: unable to write secret: {}", e.to_string());
|
|
1
|
|
}, |_| {
|
|
println!("CSR written to '{}', certificate secret to '{}'", csr_path, secret_path);
|
|
0
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
fn sign<'a>(store: &Arc<Store>, cli_args: &ArgMatches<'a>) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn verify<'a>(store: &Arc<Store>, cli_args: &ArgMatches<'a>) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn import<'a>(store: &Arc<Store>, cli_args: &ArgMatches<'a>) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn factoryreset(store: &Arc<Store>) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn export<'a>(store: &Arc<Store>, cli_args: &ArgMatches<'a>) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn delete<'a>(store: &Arc<Store>, cli_args: &ArgMatches<'a>) -> i32 {
|
|
0
|
|
}
|
|
|
|
pub(crate) fn run(store: Arc<Store>, global_flags: GlobalFlags, cli_args: &ArgMatches) -> i32 {
|
|
match cli_args.subcommand() {
|
|
("list", None) => list(&store),
|
|
("show", Some(sub_cli_args)) => show(&store, &global_flags, sub_cli_args),
|
|
("newsuid", sub_cli_args) => newsuid(sub_cli_args),
|
|
("newcsr", Some(sub_cli_args)) => newcsr(sub_cli_args),
|
|
("sign", Some(sub_cli_args)) => sign(&store, sub_cli_args),
|
|
("verify", Some(sub_cli_args)) => verify(&store, sub_cli_args),
|
|
("import", Some(sub_cli_args)) => import(&store, sub_cli_args),
|
|
("export", Some(sub_cli_args)) => export(&store, sub_cli_args),
|
|
("delete", Some(sub_cli_args)) => delete(&store, sub_cli_args),
|
|
("factoryreset", None) => factoryreset(&store),
|
|
_ => {
|
|
crate::print_help();
|
|
1
|
|
}
|
|
}
|
|
}
|