cleaned up more, added root cert generation functionality (for testing mostly), changed .gitignore

This commit is contained in:
aiden 2023-05-07 03:49:19 +01:00
parent 93274fc906
commit 57dc94af88
Signed by: aiden
GPG Key ID: EFA9C74AEBF806E0
9 changed files with 468 additions and 330 deletions

2
.gitignore vendored

@ -1,2 +1,2 @@
/target
test.json

@ -30,9 +30,18 @@ pub struct AcmeOrder {
pub certificate: Option<String>,
}
#[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";
}

@ -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<HttpClient>,
keypair: &'a pkey::PKey<pkey::Private>,
key_id: Option<String>,
pub struct AcmecContext {
http_client: LazyMut<HttpClient>,
keypair: PKey<Private>,
key_id: Option<Rc<String>>,
}
impl<'a> AcmecContext<'a> {
pub fn new(keypair: &'a PKey<pkey::Private>) -> Self {
impl AcmecContext {
pub fn new(keypair: PKey<Private>) -> 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<OsString>) -> Result<Self, &'static str> {
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<pkey::Private> {
return self.keypair;
pub fn http_client(&mut self) -> &mut HttpClient {
return &mut(self.http_client);
}
pub fn keypair(&self) -> &PKey<Private> {
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<Rc<String>> {
return self.key_id.clone();
}
}

@ -2,7 +2,7 @@ use std::{fs::File, io::{Seek, Write}};
pub struct CleanFile {
path: String,
file: File,
file: Option<File>,
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<T: AsRef<[u8]>>(&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 {

@ -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<u8>,
pub kid: String,
}
#[derive(Debug)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct OrderDetails {
pub url: String,
pub challenges: Vec<String>,
pub finalize: String,
pub dns_names: Vec<String>,
}
#[derive(Debug)]
#[derive(serde::Serialize, serde::Deserialize)]
struct AcmecConfig {
// account
account_details: RefCell<Option<AccountDetails>>,
// current pending order
order_details: RefCell<Option<OrderDetails>>,
pkey_pem: RefCell<Option<Vec<u8>>>,
}
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<CleanFile>,
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<Self, &'static str> {
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<AccountDetails>> {
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<OrderDetails>> {
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<Vec<u8>>> {
return self.config.pkey_pem.borrow();
}
pub fn set_pkey_pem(&self, pkey_pem: Vec<u8>) -> 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();
}
}

@ -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<Option<AccountDetails>>,
// current pending order
order_details: RefCell<Option<OrderDetails>>,
pkey_pem: RefCell<Option<Vec<u8>>>,
changed: RefCell<bool>,
}
impl AcmecConfig {
fn mutate<T>(&self, target: &RefCell<Option<T>>, new: Option<T>) {
*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<AccountDetails>> {
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<OrderDetails>> {
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<Vec<u8>>> {
return self.pkey_pem.borrow();
}
pub fn set_pkey_pem(&self, pkey_pem: Vec<u8>) {
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),
};
}
}

62
src/config_file/mod.rs Normal file

@ -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<u8>,
pub kid: String,
}
#[derive(Debug)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct OrderDetails {
pub url: String,
pub challenges: Vec<String>,
pub finalize: String,
pub dns_names: Vec<String>,
}
pub struct ConfigFile {
clean_file: Option<CleanFile>,
config: AcmecConfig,
}
impl ConfigFile {
pub fn open(path: String, create: bool) -> Result<Self, &'static str> {
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();
}
}
}

20
src/lazy_mut.rs Normal file

@ -0,0 +1,20 @@
use std::ops::{Deref, DerefMut};
#[derive(Debug)]
pub struct LazyMut<T: Default>(Option<T>);
impl<T: Default> Deref for LazyMut<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unreachable!();
}
}
impl<T: Default> DerefMut for LazyMut<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
return self.0.get_or_insert_with(|| T::default());
}
}
impl<T: Default> Default for LazyMut<T> {
fn default() -> Self {
return Self(None);
}
}

@ -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<T: serde::ser::Serialize>(s: T) -> Result<String, &'static str> {
return serde_json::to_string(&s).map_err(|_| "failed to stringify");
}
@ -61,11 +63,34 @@ fn decode_response<T: serde::de::DeserializeOwned>(resp: HttpResponse) -> Result
return resp.json().map_err(|_| "failed to decode response");
}
fn gen_keypair() -> Result<PKey<Private>, &'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<Private>, passphrase: Option<OsString>) -> Result<Vec<u8>, &'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<OsString>) -> Result<PKey<Private>, &'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<T: AsRef<[u8]>>(u8s: T) -> String {
return b64e(u8s.as_ref()).replace("+", "-").replace("/", "_").replace("=", "");
}
fn ektyn(kp: &PKey<pkey::Private>) -> Result<String, &'static str> {
fn ektyn(kp: &PKey<Private>) -> Result<String, &'static str> {
let rsa = kp
.rsa() // why does everything have to fucking copy
.map_err(|_| "kp.rsa() failed")?;
@ -74,7 +99,7 @@ fn ektyn(kp: &PKey<pkey::Private>) -> Result<String, &'static str> {
return Ok(format!(r#"{{"e":{e},"kty":"RSA","n":{n}}}"#));
}
fn token_shit(kp: &PKey<pkey::Private>, token: String) -> Result<String, &'static str> {
fn token_shit(kp: &PKey<Private>, token: String) -> Result<String, &'static str> {
/*
* aight, so, the rfcs (namely 8555 and 7638) say pretty much the following (paraphrased):
* <shit>
@ -112,10 +137,12 @@ fn token_shit(kp: &PKey<pkey::Private>, token: String) -> Result<String, &'stati
}
fn jws(context: &AcmecContext, nonce: HeaderValue, url: &str, payload: &str) -> Result<String, &'static str> {
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<HeaderValue, &'static str> {
fn get_nonce(cl: &mut HttpClient) -> Result<HeaderValue, &'static str> {
let mut resp = cl
.head(relevant_directory::NEW_NONCE)
.send()
@ -166,20 +193,20 @@ fn get_nonce(cl: &RefMut<'_, HttpClient>) -> Result<HeaderValue, &'static str> {
return Ok(replay_nonce);
}
fn acme_post<T: AsRef<str>, P: AsRef<str>>(
context: &AcmecContext,
url: T,
fn acme_post<U: AsRef<str>, P: AsRef<str>>(
context: &mut AcmecContext,
url: U,
payload: P
) -> Result<HttpResponse, &'static str> {
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<String, &'static str> {
return Ok(b64ue(csr.to_der().map_err(|_| "failed to serialize X509Req")?));
}
fn gen_csr(kp: &PKey<pkey::Private>, dns_names: &Vec<String>) -> Result<X509Req, &'static str> {
fn gen_csr(kp: &PKey<Private>, dns_names: &Vec<String>) -> Result<X509Req, &'static str> {
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<pkey::Private>, dns_names: &Vec<String>) -> Result<X509Req,
// account //
fn create_account(context: &AcmecContext) -> Result<HeaderValue, &'static str> {
fn create_account(context: &mut AcmecContext) -> Result<HeaderValue, &'static str> {
let mut resp = acme_post(
context,
relevant_directory::NEW_ACCOUNT,
@ -230,228 +255,249 @@ fn create_account(context: &AcmecContext) -> Result<HeaderValue, &'static str> {
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<String> = 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<String> = 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(());