mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-03 19:13:43 +02:00
Can make a CSR now.
This commit is contained in:
parent
ce77372e01
commit
8aee8cb78b
6 changed files with 307 additions and 39 deletions
|
@ -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<Fingerprint>,
|
||||
}
|
||||
|
||||
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<Box<[u8]>, ResultCode> {
|
||||
pub fn new_csr(&self, uid: Option<&CertificateSubjectUniqueIdSecret>) -> Result<Vec<u8>, ResultCode> {
|
||||
let mut csr: Vec<u8> = 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -175,6 +175,17 @@ pub fn random() -> u64 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Test whether this byte array or slice is all zeroes.
|
||||
pub fn is_all_zeroes<B: AsRef<[u8]>>(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
|
||||
|
|
|
@ -93,7 +93,7 @@ Advanced Operations:
|
|||
· list List certificates at local node
|
||||
· show <serial> Show certificate details
|
||||
newsid [sid secret out] Create a new subject unique ID
|
||||
newcsr [csr out] Create a subject CSR
|
||||
newcsr <csr output path> Create a subject CSR (interactive)
|
||||
sign <csr> <identity> [cert out] Sign a CSR to create a certificate
|
||||
verify <cert> Verify certificate (not chain)
|
||||
dump <cert> 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))
|
||||
|
|
|
@ -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<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn show<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn newsid<'a>(store: &Store, cli_args: Option<&ArgMatches<'a>>, auth_token: &Option<String>) -> 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<String>) -> i32 {
|
||||
0
|
||||
fn newcsr<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> 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<CertificateSubjectUniqueIdSecret> = 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<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 = 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<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::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<CertificateSerialNo> = 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<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\" (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<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn verify<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn dump<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn import<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn restore(store: &Store, auth_token: &Option<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn export<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn delete<'a>(store: &Store, cli_args: &ArgMatches<'a>, auth_token: &Option<String>) -> 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),
|
||||
|
|
|
@ -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<P: AsRef<Path>>(path: P, limit: usize) -> std::io::Result<Vec<u8>> {
|
||||
let mut v: Vec<u8> = 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<Identity, String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue