Can make a CSR now.

This commit is contained in:
Adam Ierymenko 2021-03-11 11:55:28 -05:00
parent ce77372e01
commit 8aee8cb78b
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
6 changed files with 307 additions and 39 deletions

View file

@ -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);
}
}

View file

@ -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

View file

@ -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))

View file

@ -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),

View file

@ -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)
}
}
}

View file

@ -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 {