diff --git a/.gitignore b/.gitignore index e550700..54466f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /target -test.json + diff --git a/src/acme.rs b/src/acme.rs index 7ea4bea..f729faf 100644 --- a/src/acme.rs +++ b/src/acme.rs @@ -30,9 +30,18 @@ pub struct AcmeOrder { pub certificate: Option, } +#[cfg(debug_assertions)] +pub mod relevant_directory { + pub static NEW_NONCE: &'static str = "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce"; + pub static NEW_ACCOUNT: &'static str = "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct"; + pub static NEW_ORDER: &'static str = "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; + pub static TOS: &'static str = "https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf"; +} + +#[cfg(not(debug_assertions))] pub mod relevant_directory { pub static NEW_NONCE: &'static str = "https://acme-v02.api.letsencrypt.org/acme/new-nonce"; pub static NEW_ACCOUNT: &'static str = "https://acme-v02.api.letsencrypt.org/acme/new-acct"; pub static NEW_ORDER: &'static str = "https://acme-v02.api.letsencrypt.org/acme/new-order"; - pub static TOS: &'static str = "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"; + pub static TOS: &'static str = "https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf"; } \ No newline at end of file diff --git a/src/acmec_context.rs b/src/acmec_context.rs index 393cc5c..fd32b36 100644 --- a/src/acmec_context.rs +++ b/src/acmec_context.rs @@ -1,34 +1,46 @@ -use std::cell::{RefCell, RefMut}; +use std::{ffi::OsString, rc::Rc}; use reqwest::blocking::Client as HttpClient; -use openssl::pkey::{self, PKey}; +use openssl::pkey::{PKey, Private}; + +use crate::{ConfigFile, lazy_mut::LazyMut, pem_to_keypair}; #[derive(Debug)] -pub struct AcmecContext<'a> { - http_client: RefCell, - keypair: &'a pkey::PKey, - key_id: Option, +pub struct AcmecContext { + http_client: LazyMut, + keypair: PKey, + key_id: Option>, } -impl<'a> AcmecContext<'a> { - pub fn new(keypair: &'a PKey) -> Self { +impl AcmecContext { + pub fn new(keypair: PKey) -> Self { Self { - http_client: RefCell::new(HttpClient::new()), keypair, key_id: None, + http_client: LazyMut::default(), keypair, key_id: None, } } + pub fn with_config_file(config_file: &ConfigFile, pem_passphrase: Option) -> Result { + let borrow = config_file.account_details(); + let account_details = borrow.as_ref().ok_or("invalid config file")?; + let kp = pem_to_keypair(&(account_details.pem_kp), pem_passphrase)?; + let mut context = Self::new(kp); + context.set_key_id(account_details.kid.clone()); + drop(borrow); - pub fn http_client(&self) -> RefMut<'_, HttpClient> { - return self.http_client.borrow_mut(); + return Ok(context); } - pub fn keypair(&self) -> &pkey::PKey { - return self.keypair; + pub fn http_client(&mut self) -> &mut HttpClient { + return &mut(self.http_client); + } + + pub fn keypair(&self) -> &PKey { + return &(self.keypair); } pub fn set_key_id(&mut self, key_id: String) { - self.key_id = Some(key_id); + self.key_id = Some(Rc::new(key_id)); return; } - pub fn key_id(&self) -> Option<&String> { - return self.key_id.as_ref(); + pub fn key_id(&self) -> Option> { + return self.key_id.clone(); } } \ No newline at end of file diff --git a/src/clean_file.rs b/src/clean_file.rs index 8ca848c..c04165a 100644 --- a/src/clean_file.rs +++ b/src/clean_file.rs @@ -2,7 +2,7 @@ use std::{fs::File, io::{Seek, Write}}; pub struct CleanFile { path: String, - file: File, + file: Option, created: bool, written_to: bool, } @@ -16,27 +16,34 @@ impl CleanFile { _ => "failed to open file" })?; - return Ok(Self { path, file, created: create, written_to: false, }); + return Ok(Self { path, file: Some(file), created: create, written_to: false, }); } fn delete_file(&mut self) -> Result<(), &'static str> { - return std::fs::remove_file(&(self.path)).map_err(|_| "failed to delete file"); + return if let Some(_) = self.file.take() { + std::fs::remove_file(&(self.path)).map_err(|_| "failed to delete file") + } else { Ok(()) }; } pub fn delete(mut self) -> Result<(), &'static str> { return self.delete_file(); } pub fn write>(&mut self, bytes: T) -> Result<(), &'static str> { - self.file.set_len(0).map_err(|_| "failed to truncate file")?; - self.file.seek(std::io::SeekFrom::Start(0)).map_err(|_| "failed to seek file")?; - self.file.write_all(bytes.as_ref()).map_err(|_| "failed to write to file")?; + let file: &mut File = self.file.as_mut().ok_or("file deleted")?; + file.set_len(0).map_err(|_| "failed to truncate file")?; + file.seek(std::io::SeekFrom::Start(0)).map_err(|_| "failed to seek file")?; + file.write_all(bytes.as_ref()).map_err(|_| "failed to write to file")?; self.written_to = true; return Ok(()); } - pub fn file(&self) -> &File { - return &(self.file); + pub fn file(&self) -> Result<&File, &'static str> { + return self.file.as_ref().ok_or("failed deleted"); + } + + pub fn path(&self) -> &str { + return &(self.path); } } impl Drop for CleanFile { diff --git a/src/config_file.rs b/src/config_file.rs deleted file mode 100644 index 97d52a6..0000000 --- a/src/config_file.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::cell::{RefCell, Ref}; - -use crate::{clean_file::CleanFile, stringify_ser}; - -#[derive(Debug)] -#[derive(serde::Serialize, serde::Deserialize)] -pub struct AccountDetails { - // account key - pub pem_kp: Vec, - pub kid: String, -} - -#[derive(Debug)] -#[derive(serde::Serialize, serde::Deserialize)] -pub struct OrderDetails { - pub url: String, - pub challenges: Vec, - pub finalize: String, - pub dns_names: Vec, -} - -#[derive(Debug)] -#[derive(serde::Serialize, serde::Deserialize)] -struct AcmecConfig { - // account - account_details: RefCell>, - - // current pending order - order_details: RefCell>, - pkey_pem: RefCell>>, -} -impl Default for AcmecConfig { - fn default() -> Self { - return Self { - account_details: RefCell::new(None), - - order_details: RefCell::new(None), - pkey_pem: RefCell::new(None), - }; - } -} - -pub struct ConfigFile { - clean_file: RefCell, - config: AcmecConfig, -} -impl ConfigFile { - fn write(&self) -> Result<(), &'static str> { - return self.clean_file.borrow_mut().write(stringify_ser(&(self.config))?.as_bytes()); - } - pub fn open(path: String, create: bool) -> Result { - let clean_file = RefCell::new(CleanFile::open(path, create)?); - let config: AcmecConfig = serde_json::from_reader(clean_file.borrow().file()).unwrap_or_default(); - return Ok(Self { clean_file, config }); - } - pub fn delete(self) -> Result<(), &'static str> { - return self.clean_file.into_inner().delete(); - } - - pub fn account_details(&self) -> Ref<'_, Option> { - return self.config.account_details.borrow(); - } - pub fn set_account_details(&self, account_details: AccountDetails) -> Result<(), &'static str> { - self.config.account_details.borrow_mut().replace(account_details); - return self.write(); - } - - pub fn order_details(&self) -> Ref<'_, Option> { - return self.config.order_details.borrow(); - } - pub fn set_order_details(&self, order_details: OrderDetails) -> Result<(), &'static str> { - self.config.order_details.borrow_mut().replace(order_details); - return self.write(); - } - - pub fn pkey_pem(&self) -> Ref<'_, Option>> { - return self.config.pkey_pem.borrow(); - } - pub fn set_pkey_pem(&self, pkey_pem: Vec) -> Result<(), &'static str> { - self.config.pkey_pem.borrow_mut().replace(pkey_pem); - return self.write(); - } - - pub fn discard_order(&mut self) -> Result<(), &'static str> { - self.config.order_details.borrow_mut().take(); - self.config.pkey_pem.borrow_mut().take(); - return self.write(); - } -} \ No newline at end of file diff --git a/src/config_file/acmec_config.rs b/src/config_file/acmec_config.rs new file mode 100644 index 0000000..096c204 --- /dev/null +++ b/src/config_file/acmec_config.rs @@ -0,0 +1,71 @@ +use std::cell::{RefCell, Ref}; +use super::{AccountDetails, OrderDetails}; + +#[derive(Debug)] +#[derive(serde::Serialize, serde::Deserialize)] +pub struct AcmecConfig { + // account + account_details: RefCell>, + + // current pending order + order_details: RefCell>, + pkey_pem: RefCell>>, + + changed: RefCell, +} +impl AcmecConfig { + fn mutate(&self, target: &RefCell>, new: Option) { + *self.changed.borrow_mut() = true; + let mut borrow = target.borrow_mut(); + if let Some(new) = new { + borrow.replace(new); + } else { + borrow.take(); + } + return; + } + + pub fn account_details(&self) -> Ref<'_, Option> { + return self.account_details.borrow(); + } + pub fn set_account_details(&self, account_details: AccountDetails) { + return self.mutate(&(self.account_details), Some(account_details)); + } + + pub fn order_details(&self) -> Ref<'_, Option> { + return self.order_details.borrow(); + } + pub fn set_order_details(&self, order_details: OrderDetails) { + return self.mutate(&(&self.order_details), Some(order_details)); + } + + pub fn pkey_pem(&self) -> Ref<'_, Option>> { + return self.pkey_pem.borrow(); + } + pub fn set_pkey_pem(&self, pkey_pem: Vec) { + return self.mutate(&(self.pkey_pem), Some(pkey_pem)); + } + + pub fn discard_order(&mut self) { + self.mutate(&(self.order_details), None); + self.mutate(&(self.pkey_pem), None); + + return; + } + + pub fn changed(&self) -> bool { + return *self.changed.borrow(); + } +} +impl Default for AcmecConfig { + fn default() -> Self { + return Self { + account_details: RefCell::new(None), + + order_details: RefCell::new(None), + pkey_pem: RefCell::new(None), + + changed: RefCell::new(false), + }; + } +} \ No newline at end of file diff --git a/src/config_file/mod.rs b/src/config_file/mod.rs new file mode 100644 index 0000000..a8adc24 --- /dev/null +++ b/src/config_file/mod.rs @@ -0,0 +1,62 @@ +use std::ops::{Deref, DerefMut}; +use crate::{clean_file::CleanFile, stringify_ser}; + +mod acmec_config; +use acmec_config::AcmecConfig; + +#[derive(Debug)] +#[derive(serde::Serialize, serde::Deserialize)] +pub struct AccountDetails { + // account key + pub pem_kp: Vec, + pub kid: String, +} + +#[derive(Debug)] +#[derive(serde::Serialize, serde::Deserialize)] +pub struct OrderDetails { + pub url: String, + pub challenges: Vec, + pub finalize: String, + pub dns_names: Vec, +} + +pub struct ConfigFile { + clean_file: Option, + config: AcmecConfig, +} +impl ConfigFile { + pub fn open(path: String, create: bool) -> Result { + let clean_file = CleanFile::open(path, create)?; + let config: AcmecConfig = serde_json::from_reader(clean_file.file().unwrap()).unwrap_or_default(); + return Ok(Self { clean_file: Some(clean_file), config, }); + } + pub fn delete(mut self) -> Result<(), &'static str> { + return self.clean_file.take().unwrap().delete(); + } + pub fn path(&self) -> &str { + return self.clean_file.as_ref().unwrap().path(); + } +} +impl Deref for ConfigFile { + type Target = AcmecConfig; + fn deref(&self) -> &Self::Target { + return &(self.config); + } +} +impl DerefMut for ConfigFile { + fn deref_mut(&mut self) -> &mut Self::Target { + return &mut(self.config); + } +} +impl Drop for ConfigFile { + fn drop(&mut self) { + let Some(mut clean_file) = self.clean_file.take() else { + return; + }; + if self.changed() { + let json_config = stringify_ser(&(self.config)).unwrap(); + clean_file.write(json_config).unwrap(); + } + } +} \ No newline at end of file diff --git a/src/lazy_mut.rs b/src/lazy_mut.rs new file mode 100644 index 0000000..bfe0edc --- /dev/null +++ b/src/lazy_mut.rs @@ -0,0 +1,20 @@ +use std::ops::{Deref, DerefMut}; + +#[derive(Debug)] +pub struct LazyMut(Option); +impl Deref for LazyMut { + type Target = T; + fn deref(&self) -> &Self::Target { + unreachable!(); + } +} +impl DerefMut for LazyMut { + fn deref_mut(&mut self) -> &mut Self::Target { + return self.0.get_or_insert_with(|| T::default()); + } +} +impl Default for LazyMut { + fn default() -> Self { + return Self(None); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f17adba..73f7e37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,18 +23,18 @@ // jws uses account key // csrs embed public keys of keypairs generated per certificate; csr representations are placed in a jws payload -use std::{env, os::unix::ffi::OsStrExt, cell::RefMut, str::from_utf8}; +use std::{env, os::unix::ffi::OsStrExt, str::from_utf8, ops::Deref, thread::sleep, time::Duration, ffi::OsString}; use openssl::{ rsa::Rsa, sign::Signer, hash::MessageDigest, - pkey::{self, PKey}, + pkey::{PKey, Private}, symm::Cipher, - x509::{X509Req, extension::SubjectAlternativeName}, + x509::{X509Req, extension::SubjectAlternativeName, X509Builder}, stack::Stack, - base64::encode_block as b64e, + base64::encode_block as b64e, asn1::Asn1Time, }; use reqwest::{self, header::HeaderValue, blocking::{Client as HttpClient, Response as HttpResponse}}; use {serde, serde_json}; @@ -51,6 +51,8 @@ use config_file::*; mod acmec_context; use acmec_context::AcmecContext; +mod lazy_mut; + fn stringify_ser(s: T) -> Result { return serde_json::to_string(&s).map_err(|_| "failed to stringify"); } @@ -61,11 +63,34 @@ fn decode_response(resp: HttpResponse) -> Result return resp.json().map_err(|_| "failed to decode response"); } +fn gen_keypair() -> Result, &'static str> { + return Rsa::generate(2048) + .and_then(|keypair| PKey::from_rsa(keypair)) + .map_err(|_| "failed to generate rsa keypair"); +} +fn privkey_to_pem(kp: &PKey, passphrase: Option) -> Result, &'static str> { + return if let Some(passphrase) = passphrase { + kp.private_key_to_pem_pkcs8_passphrase( + Cipher::aes_256_cbc(), + passphrase.as_os_str().as_bytes() + ) + } else { + kp.private_key_to_pem_pkcs8() + }.map_err(|_| "failed to encode private key as pem"); +} +fn pem_to_keypair(pem: &[u8], passphrase: Option) -> Result, &'static str> { + return if let Some(passphrase) = passphrase { + PKey::private_key_from_pem_passphrase(pem, passphrase.as_os_str().as_bytes()) + } else { + PKey::private_key_from_pem(pem) + }.map_err(|_| "failed to decode account keypair pem"); +} + fn b64ue>(u8s: T) -> String { return b64e(u8s.as_ref()).replace("+", "-").replace("/", "_").replace("=", ""); } -fn ektyn(kp: &PKey) -> Result { +fn ektyn(kp: &PKey) -> Result { let rsa = kp .rsa() // why does everything have to fucking copy .map_err(|_| "kp.rsa() failed")?; @@ -74,7 +99,7 @@ fn ektyn(kp: &PKey) -> Result { return Ok(format!(r#"{{"e":{e},"kty":"RSA","n":{n}}}"#)); } -fn token_shit(kp: &PKey, token: String) -> Result { +fn token_shit(kp: &PKey, token: String) -> Result { /* * aight, so, the rfcs (namely 8555 and 7638) say pretty much the following (paraphrased): * @@ -112,10 +137,12 @@ fn token_shit(kp: &PKey, token: String) -> Result Result { + let keypair = context.keypair(); + let key = if let Some(url) = context.key_id() { - format!(r#""kid":{}"#, stringify(url)?) + format!(r#""kid":{}"#, stringify(url.deref())?) } else { - format!(r#""jwk":{}"#, ektyn(context.keypair())?) + format!(r#""jwk":{}"#, ektyn(keypair)?) }; let nonce = stringify( @@ -136,7 +163,7 @@ fn jws(context: &AcmecContext, nonce: HeaderValue, url: &str, payload: &str) -> let body = b64ue(payload.as_bytes()); let data_to_sign = format!("{}.{}", header, body); - let mut signer = Signer::new(MessageDigest::sha256(), context.keypair()).map_err(|_| "Signer::new(...) failed")?; + let mut signer = Signer::new(MessageDigest::sha256(), keypair).map_err(|_| "Signer::new(...) failed")?; signer.update(data_to_sign.as_bytes()).map_err(|_| "signer.update(...) failed")?; let signature = stringify( b64ue( @@ -156,7 +183,7 @@ fn jws(context: &AcmecContext, nonce: HeaderValue, url: &str, payload: &str) -> )); } -fn get_nonce(cl: &RefMut<'_, HttpClient>) -> Result { +fn get_nonce(cl: &mut HttpClient) -> Result { let mut resp = cl .head(relevant_directory::NEW_NONCE) .send() @@ -166,20 +193,20 @@ fn get_nonce(cl: &RefMut<'_, HttpClient>) -> Result { return Ok(replay_nonce); } -fn acme_post, P: AsRef>( - context: &AcmecContext, - - url: T, +fn acme_post, P: AsRef>( + context: &mut AcmecContext, + + url: U, payload: P ) -> Result { - let cl = context.http_client(); + let nonce = get_nonce(context.http_client())?; let body = jws( - &(context), - get_nonce(&(cl))?, + context, + nonce, url.as_ref(), payload.as_ref() )?; - let resp = cl + let resp = context.http_client() .post(url.as_ref()) .body(body) .header("content-type", "application/jose+json") @@ -197,15 +224,13 @@ const NO_PAYLOAD: &'static str = ""; fn b64ue_csr(csr: X509Req) -> Result { return Ok(b64ue(csr.to_der().map_err(|_| "failed to serialize X509Req")?)); } -fn gen_csr(kp: &PKey, dns_names: &Vec) -> Result { +fn gen_csr(kp: &PKey, dns_names: &Vec) -> Result { let mut builder = X509Req::builder().map_err(|_| "X509::builder() failed")?; let mut alt_names = SubjectAlternativeName::new(); - for dns_name in dns_names { - alt_names.dns(&(dns_name)); - } - let built_alt_names = alt_names.build(&(builder.x509v3_context(None))).map_err(|_| "alt_names.build(...) failed")?; + dns_names.into_iter().for_each(|dns_name| { alt_names.dns(&(dns_name)); }); + let alt_names = alt_names.build(&(builder.x509v3_context(None))).map_err(|_| "alt_names.build(...) failed")?; let mut stack = Stack::new().map_err(|_| "Stack::new() failed")?; - stack.push(built_alt_names).map_err(|_| "stack.push(...) failed")?; + stack.push(alt_names).map_err(|_| "stack.push(...) failed")?; builder.add_extensions(&(stack)).map_err(|_| "builder.add_extensions(...) failed")?; builder.set_pubkey(&(kp)).map_err(|_| "builder.set_pubkey(...) failed")?; // yes, this really will set the public key builder.sign(&(kp), MessageDigest::sha256()).map_err(|_| "builder.sign(...) failed")?; @@ -214,7 +239,7 @@ fn gen_csr(kp: &PKey, dns_names: &Vec) -> Result Result { +fn create_account(context: &mut AcmecContext) -> Result { let mut resp = acme_post( context, relevant_directory::NEW_ACCOUNT, @@ -230,228 +255,249 @@ fn create_account(context: &AcmecContext) -> Result { let location = headers.remove("location").ok_or("failed to get Location")?; return Ok(location); } -fn deactivate_account(context: &AcmecContext) -> Result<(), &'static str> { +fn deactivate_account(context: &mut AcmecContext) -> Result<(), &'static str> { return acme_post( context, - context.key_id().unwrap(), + context.key_id().unwrap().deref(), r#"{ "status": "deactivated" }"# ).map(|_| ()); } fn main() -> Result<(), &'static str> { let pem_passphrase = env::var_os("ACMEC_PASSPHRASE"); + let pkey_passphrase = env::var_os("ACMEC_PKEY_PASSPHRASE"); let mut args_iter = env::args(); args_iter.next().ok_or("expected program path")?; - let path_to_config = args_iter.next().ok_or("expected a config path")?; + let config_path = args_iter.next().ok_or("expected a config path")?; let action = args_iter.next().ok_or("expected an action")?; - let mut config_file = ConfigFile::open(path_to_config, action == "create")?; - - if action == "create" { - if args_iter.next().as_deref() != Some("accept") { - eprintln!("use `create accept` to accept the terms of service at {}", relevant_directory::TOS); - return Err("terms of service not agreed to"); - } - - let kp = Rsa::generate(2048).and_then(|keypair| PKey::from_rsa(keypair)).map_err(|_| "failed to generate rsa keypair")?; - let context = AcmecContext::new(&(kp)); - - let header = create_account(&(context))?; - let kid = header.to_str().map_err(|_| "invalid key id")?.to_owned(); - - let pem_kp = if let Some(passphrase) = pem_passphrase { - kp.private_key_to_pem_pkcs8_passphrase( - Cipher::aes_256_cbc(), - passphrase.as_os_str().as_bytes() - ) - } else { - kp.private_key_to_pem_pkcs8() - }.map_err(|_| "failed to encode keypair as pem")?; - - config_file.set_account_details(AccountDetails { - pem_kp, kid, - })?; - - return Ok(()); - } - - let borrow = config_file.account_details(); - let account_details = borrow.as_ref().ok_or("invalid config file")?; - let kp = if let Some(passphrase) = pem_passphrase { - PKey::private_key_from_pem_passphrase(&(account_details.pem_kp), passphrase.as_os_str().as_bytes()) - } else { - PKey::private_key_from_pem(&(account_details.pem_kp)) - }.map_err(|_| "failed to decode account keypair pem")?; - let mut context = AcmecContext::new(&(kp)); - context.set_key_id(account_details.kid.clone()); - drop(borrow); - match action.as_str() { + "create" => { + if let None = pem_passphrase { + eprintln!("using the ACMEC_PASSPHRASE environment variable is highly recommended."); + } + + if args_iter.next().as_deref() != Some("accept") { + eprintln!("use `create accept` to accept the terms of service at {}", relevant_directory::TOS); + return Err("terms of service not agreed to"); + } + + let config_file = ConfigFile::open(config_path, true)?; + + let kp = gen_keypair()?; + let pem_kp = privkey_to_pem(&(kp), pem_passphrase)?; + + let mut context = AcmecContext::new(kp); + let header = create_account(&mut(context))?; + let kid = header.to_str().map_err(|_| "invalid key id")?.to_owned(); + + config_file.set_account_details(AccountDetails { + pem_kp, kid, + }); + + return Ok(()); + }, "delete" => { - deactivate_account(&(context))?; + let config_file = ConfigFile::open(config_path, false)?; + let mut context = AcmecContext::with_config_file(&(config_file), pem_passphrase)?; + + println!("REALLY delete \"{}\"? this cannot be undone! [yes / any other line]", config_file.path()); + let mut buf = String::new(); + std::io::stdin().read_line(&mut(buf)).map_err(|_| "failed to read line from stdin")?; + if buf != "yes\n" { + return Err("aborted; did not delete account"); + } + + deactivate_account(&mut(context))?; config_file.delete()?; } - "order" => match args_iter.next().as_deref() { - Some("place") => { - config_file.order_details().as_ref().map_or(Ok(()), |_| Err("there is already an order pending"))?; + "order" => { + let mut config_file = ConfigFile::open(config_path, false)?; + let mut context = AcmecContext::with_config_file(&(config_file), pem_passphrase)?; + match args_iter.next().as_deref() { + Some("place") => { + config_file.order_details().as_ref().map_or(Ok(()), |_| Err("there is already an order pending"))?; - let mut payload = String::from(r#"{"identifiers":["#); - let dns_names: Vec = args_iter.collect(); - let mut iter = dns_names.iter(); - let mut dns_name = iter.next().ok_or("no dns names were passed")?; - loop { - payload += &(format!(r#"{{"type":"dns","value":{}}}"#, stringify(dns_name)?)); - if let Some(next) = iter.next() { - dns_name = &(next); - payload.push(','); - } else { - break; - } - } - payload.push_str("]}"); - let mut resp = acme_post( - &(&context), - relevant_directory::NEW_ORDER, - payload - )?; - if !resp.status().is_success() { - if let Ok(err) = resp.text() { - eprintln!("error: {err}"); - } - return Err("request failed"); - } - let headers = resp.headers_mut(); - let location = headers.remove("location").ok_or("failed to get Location")?; - let url = location.to_str().map_err(|_| "invalid header")?.to_string(); - let order: AcmeOrder = decode_response(resp)?; - - let mut challenge_urls = Vec::new(); - let mut output_string = String::new(); - for auth_url in &(order.authorizations) { - let resp = acme_post(&(context), auth_url, NO_PAYLOAD)?; - let auth: AcmeAuthorization = decode_response(resp)?; - let challenge: AcmeChallenge = auth.challenges.into_iter().find(|challenge| &(challenge.r#type) == "dns-01").ok_or("no dns-01 challenge")?; - challenge_urls.push(challenge.url); - output_string += &(format!("_acme-challenge.{} {}", auth.identifier.value, token_shit(&(kp), challenge.token)?)); - } - config_file.set_order_details(OrderDetails { - url, - challenges: challenge_urls, - finalize: order.finalize.to_string(), - dns_names, - })?; - println!("{}", output_string); - return Ok(()); - }, - Some("finalize") => { - let borrow = config_file.order_details(); - let Some(order) = borrow.as_ref() else { - return Err("no order pending"); - }; - - let cert_path = args_iter.next().ok_or("expected path to cert file")?; - let pkey_path = args_iter.next().ok_or("expected path to pkey file")?; - - let mut cert_file = CleanFile::open(cert_path, true)?; - let mut pkey_file = CleanFile::open(pkey_path, true)?; - - let pkey_passphrase = env::var_os("ACMEC_PKEY_PASSPHRASE"); - for url in &(order.challenges) { - acme_post(&(context), &(url), r#"{}"#)?; - } - loop { - let resp = acme_post(&(context), &(order.url), NO_PAYLOAD)?; - let acme_order: AcmeOrder = decode_response(resp)?; - match acme_order.status.as_str() { - "ready" => break, - "pending" => (), - status => { - eprintln!("order status: {status}"); - drop(borrow); - config_file.discard_order()?; - return Err("bad order status"); + let mut payload = String::from(r#"{"identifiers":["#); + let dns_names: Vec = args_iter.collect(); + let mut iter = dns_names.iter(); + let mut dns_name = iter.next().ok_or("no dns names were passed")?; + loop { + payload += &(format!(r#"{{"type":"dns","value":{}}}"#, stringify(dns_name)?)); + if let Some(next) = iter.next() { + dns_name = &(next); + payload.push(','); + } else { + break; } } - std::thread::sleep(std::time::Duration::from_secs(3)); - } + payload.push_str("]}"); + let mut resp = acme_post( + &mut(context), + relevant_directory::NEW_ORDER, + payload + )?; + if !resp.status().is_success() { + if let Ok(err) = resp.text() { + eprintln!("error: {err}"); + } + return Err("request failed"); + } + let headers = resp.headers_mut(); + let location = headers.remove("location").ok_or("failed to get Location")?; + let url = location.to_str().map_err(|_| "invalid header")?.to_string(); + let order: AcmeOrder = decode_response(resp)?; + + let mut challenge_urls = Vec::new(); + let mut output_string = String::new(); + for auth_url in &(order.authorizations) { + let resp = acme_post(&mut(context), auth_url, NO_PAYLOAD)?; + let auth: AcmeAuthorization = decode_response(resp)?; + let challenge: AcmeChallenge = auth.challenges.into_iter().find(|challenge| &(challenge.r#type) == "dns-01").ok_or("no dns-01 challenge")?; + challenge_urls.push(challenge.url); + output_string += &(format!("_acme-challenge.{} {}", auth.identifier.value, token_shit(context.keypair(), challenge.token)?)); + } + config_file.set_order_details(OrderDetails { + url, + challenges: challenge_urls, + finalize: order.finalize.to_string(), + dns_names, + }); + println!("{}", output_string); + return Ok(()); + }, + Some("finalize") => { + let borrow = config_file.order_details(); + let Some(order) = borrow.as_ref() else { + return Err("no order pending"); + }; - let mut pem_borrow = config_file.pkey_pem(); - let (cert_kp, pkey_pem) = if let Some(pkey_pem) = pem_borrow.as_ref() { - let cert_kp = if let Some(passphrase) = &(pkey_passphrase) { - PKey::private_key_from_pem_passphrase(&(pkey_pem), passphrase.as_os_str().as_bytes()) + let cert_path = args_iter.next().ok_or("expected path to cert file")?; + let pkey_path = args_iter.next().ok_or("expected path to pkey file")?; + + let mut cert_file = CleanFile::open(cert_path, true)?; + let mut pkey_file = CleanFile::open(pkey_path, true)?; + + for url in &(order.challenges) { + acme_post(&mut(context), &(url), r#"{}"#)?; + } + loop { + let resp = acme_post(&mut(context), &(order.url), NO_PAYLOAD)?; + let acme_order: AcmeOrder = decode_response(resp)?; + match acme_order.status.as_str() { + "ready" => break, + "pending" => (), + status => { + eprintln!("order status: {status}"); + drop(borrow); + config_file.discard_order(); + return Err("bad order status"); + } + } + sleep(Duration::from_secs(3)); + } + + let mut pem_borrow = config_file.pkey_pem(); + let (cert_kp, pkey_pem) = if let Some(pkey_pem) = pem_borrow.as_ref() { + (pem_to_keypair(&(pkey_pem), pkey_passphrase)?, pkey_pem) } else { - PKey::private_key_from_pem(&(pkey_pem)) - }.map_err(|_| "failed to decode certificate keypair pem")?; + let cert_kp = gen_keypair()?; - (cert_kp, pkey_pem) - } else { - let cert_kp = Rsa::generate(2048).and_then(|keypair| PKey::from_rsa(keypair)).map_err(|_| "failed to generate rsa keypair")?; + drop(pem_borrow); + config_file.set_pkey_pem(privkey_to_pem(&(cert_kp), pkey_passphrase)?); - drop(pem_borrow); - config_file.set_pkey_pem( - if let Some(passphrase) = &(pkey_passphrase) { - cert_kp.private_key_to_pem_pkcs8_passphrase(Cipher::aes_256_cbc(), passphrase.as_os_str().as_bytes()) - } else { - cert_kp.private_key_to_pem_pkcs8() - }.map_err(|_| "failed to serialize private key")? + pem_borrow = config_file.pkey_pem(); + (cert_kp, pem_borrow.as_ref().unwrap()) + }; + + let pkey_pem_view = from_utf8(&(pkey_pem)).map_err(|_| "invalid utf-8 bytes in pem-encoded private key")?; + + acme_post( + &mut(context), &(order.finalize), + + format!( + r#"{{ "csr": {} }}"#, + stringify(b64ue_csr(gen_csr(&(cert_kp), &(order.dns_names))?)?)? + ) )?; - pem_borrow = config_file.pkey_pem(); - (cert_kp, pem_borrow.as_ref().unwrap()) - }; + let acme_order = loop { + let resp = acme_post(&mut(context), &(order.url), NO_PAYLOAD)?; + let order: AcmeOrder = decode_response(resp)?; + match order.status.as_str() { + "valid" => break order, + "processing" => (), + status => { + eprintln!("order status: {status}"); + // safe to discard order i think + drop(borrow); + drop(pem_borrow); + config_file.discard_order(); + return Err("bad order status"); + } + } + sleep(Duration::from_secs(3)); + }; - let pkey_pem_view = from_utf8(&(pkey_pem)).map_err(|_| "invalid utf-8 bytes in pem-encoded private key")?; + let cert = acme_post(&mut(context), &(acme_order.certificate.ok_or("expected acme certificate")?), NO_PAYLOAD)? + .text() + .map_err(|_| "failed to read response")?; - acme_post( - &(context), &(order.finalize), - - format!( - r#"{{ "csr": {} }}"#, - stringify(b64ue_csr(gen_csr(&(cert_kp), &(order.dns_names))?)?)? - ) - )?; - - let acme_order = loop { - let resp = acme_post(&(context), &(order.url), NO_PAYLOAD)?; - let order: AcmeOrder = decode_response(resp)?; - match order.status.as_str() { - "valid" => break order, - "processing" => (), - status => { - eprintln!("order status: {status}"); - // safe to discard order i think - drop(borrow); - drop(pem_borrow); - config_file.discard_order()?; - return Err("bad order status"); - } + if let Err(_) = cert_file.write(&(cert)) { + eprintln!("failed to write certificate to file, printing to stdout instead"); + println!("{}", cert); + } + if let Err(_) = pkey_file.write(&*(pkey_pem)) { + eprintln!("failed to write private key to file, printing to stdout instead"); + println!("{}", pkey_pem_view); } - std::thread::sleep(std::time::Duration::from_secs(3)); - }; - let cert = acme_post(&(context), &(acme_order.certificate.ok_or("expected acme certificate")?), NO_PAYLOAD)? - .text() - .map_err(|_| "failed to read response")?; - - if let Err(_) = cert_file.write(&(cert)) { - eprintln!("failed to write certificate to file, printing to stdout instead"); - println!("{}", cert); - } - if let Err(_) = pkey_file.write(&*(pkey_pem)) { - eprintln!("failed to write private key to file, printing to stdout instead"); - println!("{}", pkey_pem_view); - } - - drop(borrow); - drop(pem_borrow); - config_file.discard_order()?; - return Ok(()); - }, - _ => return Err("valid subactions for order: place, finalize"), + drop(borrow); + drop(pem_borrow); + config_file.discard_order(); + return Ok(()); + }, + _ => return Err("valid subactions for order: place, finalize"), + } + } + other => match args_iter.next().as_deref() { + Some("test") => { + let (cert_path, key_path) = (config_path, other.to_owned()); + + let kp = gen_keypair()?; + let priv_pem = privkey_to_pem(&(kp), pkey_passphrase)?; + + let mut builder = X509Builder::new().map_err(|_| "X509Builder::new() failed")?; + builder.set_pubkey(&(kp)).map_err(|_| "builder.set_pubkey(...) failed")?; + + let year_from_now = Asn1Time::days_from_now(365).map_err(|_| "Asn1Time::days_from_now(365) failed")?; + builder.set_not_before(&(Asn1Time::from_unix(0).unwrap())).unwrap(); + builder.set_not_after(&(year_from_now)).unwrap(); + + let mut alt_names = SubjectAlternativeName::new(); + args_iter.for_each(|dns_name| { alt_names.dns(&(dns_name)); }); + let alt_names = alt_names.build(&(builder.x509v3_context(None, None))).map_err(|_| "alt_names.build(...) failed")?; + builder.append_extension(alt_names).map_err(|_| "builder.append_extension(...) failed")?; + + builder.sign(&(kp), MessageDigest::sha256()).unwrap(); + let cert_pem = builder.build().to_pem().map_err(|_| "failed to encode certificate as pem")?; + + let mut cert_file = CleanFile::open(cert_path, true)?; + let mut priv_file = CleanFile::open(key_path, true)?; + + if let Err(_) = cert_file.write(&(cert_pem)) { + eprintln!("failed to write certificate! printing to stdout instead..."); + println!("{}", from_utf8(&(cert_pem)).unwrap()); + } + if let Err(_) = priv_file.write(&(priv_pem)) { + eprintln!("failed to write private key! printing to stdout instead..."); + println!("{}", from_utf8(&(priv_pem)).unwrap()); + } + } + _ => return Err("valid actions: create, delete, order") } - _ => return Err("valid actions: create, delete, order"), } return Ok(());