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 /target
test.json

@ -30,9 +30,18 @@ pub struct AcmeOrder {
pub certificate: Option<String>, 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 mod relevant_directory {
pub static NEW_NONCE: &'static str = "https://acme-v02.api.letsencrypt.org/acme/new-nonce"; 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_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 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 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)] #[derive(Debug)]
pub struct AcmecContext<'a> { pub struct AcmecContext {
http_client: RefCell<HttpClient>, http_client: LazyMut<HttpClient>,
keypair: &'a pkey::PKey<pkey::Private>, keypair: PKey<Private>,
key_id: Option<String>, key_id: Option<Rc<String>>,
} }
impl<'a> AcmecContext<'a> { impl AcmecContext {
pub fn new(keypair: &'a PKey<pkey::Private>) -> Self { pub fn new(keypair: PKey<Private>) -> Self {
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 Ok(context);
return self.http_client.borrow_mut();
} }
pub fn keypair(&self) -> &pkey::PKey<pkey::Private> { pub fn http_client(&mut self) -> &mut HttpClient {
return self.keypair; return &mut(self.http_client);
}
pub fn keypair(&self) -> &PKey<Private> {
return &(self.keypair);
} }
pub fn set_key_id(&mut self, key_id: String) { 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; return;
} }
pub fn key_id(&self) -> Option<&String> { pub fn key_id(&self) -> Option<Rc<String>> {
return self.key_id.as_ref(); return self.key_id.clone();
} }
} }

@ -2,7 +2,7 @@ use std::{fs::File, io::{Seek, Write}};
pub struct CleanFile { pub struct CleanFile {
path: String, path: String,
file: File, file: Option<File>,
created: bool, created: bool,
written_to: bool, written_to: bool,
} }
@ -16,27 +16,34 @@ impl CleanFile {
_ => "failed to open file" _ => "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> { 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> { pub fn delete(mut self) -> Result<(), &'static str> {
return self.delete_file(); return self.delete_file();
} }
pub fn write<T: AsRef<[u8]>>(&mut self, bytes: T) -> Result<(), &'static str> { pub fn write<T: AsRef<[u8]>>(&mut self, bytes: T) -> Result<(), &'static str> {
self.file.set_len(0).map_err(|_| "failed to truncate file")?; let file: &mut File = self.file.as_mut().ok_or("file deleted")?;
self.file.seek(std::io::SeekFrom::Start(0)).map_err(|_| "failed to seek file")?; file.set_len(0).map_err(|_| "failed to truncate file")?;
self.file.write_all(bytes.as_ref()).map_err(|_| "failed to write to 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; self.written_to = true;
return Ok(()); return Ok(());
} }
pub fn file(&self) -> &File { pub fn file(&self) -> Result<&File, &'static str> {
return &(self.file); return self.file.as_ref().ok_or("failed deleted");
}
pub fn path(&self) -> &str {
return &(self.path);
} }
} }
impl Drop for CleanFile { 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 // jws uses account key
// csrs embed public keys of keypairs generated per certificate; csr representations are placed in a jws payload // 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::{ use openssl::{
rsa::Rsa, rsa::Rsa,
sign::Signer, sign::Signer,
hash::MessageDigest, hash::MessageDigest,
pkey::{self, PKey}, pkey::{PKey, Private},
symm::Cipher, symm::Cipher,
x509::{X509Req, extension::SubjectAlternativeName}, x509::{X509Req, extension::SubjectAlternativeName, X509Builder},
stack::Stack, 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 reqwest::{self, header::HeaderValue, blocking::{Client as HttpClient, Response as HttpResponse}};
use {serde, serde_json}; use {serde, serde_json};
@ -51,6 +51,8 @@ use config_file::*;
mod acmec_context; mod acmec_context;
use acmec_context::AcmecContext; use acmec_context::AcmecContext;
mod lazy_mut;
fn stringify_ser<T: serde::ser::Serialize>(s: T) -> Result<String, &'static str> { fn stringify_ser<T: serde::ser::Serialize>(s: T) -> Result<String, &'static str> {
return serde_json::to_string(&s).map_err(|_| "failed to stringify"); 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"); 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 { fn b64ue<T: AsRef<[u8]>>(u8s: T) -> String {
return b64e(u8s.as_ref()).replace("+", "-").replace("/", "_").replace("=", ""); 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 let rsa = kp
.rsa() // why does everything have to fucking copy .rsa() // why does everything have to fucking copy
.map_err(|_| "kp.rsa() failed")?; .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}}}"#)); 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): * aight, so, the rfcs (namely 8555 and 7638) say pretty much the following (paraphrased):
* <shit> * <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> { 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() { let key = if let Some(url) = context.key_id() {
format!(r#""kid":{}"#, stringify(url)?) format!(r#""kid":{}"#, stringify(url.deref())?)
} else { } else {
format!(r#""jwk":{}"#, ektyn(context.keypair())?) format!(r#""jwk":{}"#, ektyn(keypair)?)
}; };
let nonce = stringify( let nonce = stringify(
@ -136,7 +163,7 @@ fn jws(context: &AcmecContext, nonce: HeaderValue, url: &str, payload: &str) ->
let body = b64ue(payload.as_bytes()); let body = b64ue(payload.as_bytes());
let data_to_sign = format!("{}.{}", header, body); 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")?; signer.update(data_to_sign.as_bytes()).map_err(|_| "signer.update(...) failed")?;
let signature = stringify( let signature = stringify(
b64ue( 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 let mut resp = cl
.head(relevant_directory::NEW_NONCE) .head(relevant_directory::NEW_NONCE)
.send() .send()
@ -166,20 +193,20 @@ fn get_nonce(cl: &RefMut<'_, HttpClient>) -> Result<HeaderValue, &'static str> {
return Ok(replay_nonce); return Ok(replay_nonce);
} }
fn acme_post<T: AsRef<str>, P: AsRef<str>>( fn acme_post<U: AsRef<str>, P: AsRef<str>>(
context: &AcmecContext, context: &mut AcmecContext,
url: T, url: U,
payload: P payload: P
) -> Result<HttpResponse, &'static str> { ) -> Result<HttpResponse, &'static str> {
let cl = context.http_client(); let nonce = get_nonce(context.http_client())?;
let body = jws( let body = jws(
&(context), context,
get_nonce(&(cl))?, nonce,
url.as_ref(), url.as_ref(),
payload.as_ref() payload.as_ref()
)?; )?;
let resp = cl let resp = context.http_client()
.post(url.as_ref()) .post(url.as_ref())
.body(body) .body(body)
.header("content-type", "application/jose+json") .header("content-type", "application/jose+json")
@ -197,15 +224,13 @@ const NO_PAYLOAD: &'static str = "";
fn b64ue_csr(csr: X509Req) -> Result<String, &'static str> { fn b64ue_csr(csr: X509Req) -> Result<String, &'static str> {
return Ok(b64ue(csr.to_der().map_err(|_| "failed to serialize X509Req")?)); 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 builder = X509Req::builder().map_err(|_| "X509::builder() failed")?;
let mut alt_names = SubjectAlternativeName::new(); let mut alt_names = SubjectAlternativeName::new();
for dns_name in dns_names { dns_names.into_iter().for_each(|dns_name| { alt_names.dns(&(dns_name)); });
alt_names.dns(&(dns_name)); let alt_names = alt_names.build(&(builder.x509v3_context(None))).map_err(|_| "alt_names.build(...) failed")?;
}
let built_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")?; 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.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.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")?; 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 // // 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( let mut resp = acme_post(
context, context,
relevant_directory::NEW_ACCOUNT, 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")?; let location = headers.remove("location").ok_or("failed to get Location")?;
return Ok(location); return Ok(location);
} }
fn deactivate_account(context: &AcmecContext) -> Result<(), &'static str> { fn deactivate_account(context: &mut AcmecContext) -> Result<(), &'static str> {
return acme_post( return acme_post(
context, context,
context.key_id().unwrap(), context.key_id().unwrap().deref(),
r#"{ "status": "deactivated" }"# r#"{ "status": "deactivated" }"#
).map(|_| ()); ).map(|_| ());
} }
fn main() -> Result<(), &'static str> { fn main() -> Result<(), &'static str> {
let pem_passphrase = env::var_os("ACMEC_PASSPHRASE"); let pem_passphrase = env::var_os("ACMEC_PASSPHRASE");
let pkey_passphrase = env::var_os("ACMEC_PKEY_PASSPHRASE");
let mut args_iter = env::args(); let mut args_iter = env::args();
args_iter.next().ok_or("expected program path")?; 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 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() { 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" => { "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()?; config_file.delete()?;
} }
"order" => match args_iter.next().as_deref() { "order" => {
Some("place") => { let mut config_file = ConfigFile::open(config_path, false)?;
config_file.order_details().as_ref().map_or(Ok(()), |_| Err("there is already an order pending"))?; 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 mut payload = String::from(r#"{"identifiers":["#);
let dns_names: Vec<String> = args_iter.collect(); let dns_names: Vec<String> = args_iter.collect();
let mut iter = dns_names.iter(); let mut iter = dns_names.iter();
let mut dns_name = iter.next().ok_or("no dns names were passed")?; let mut dns_name = iter.next().ok_or("no dns names were passed")?;
loop { loop {
payload += &(format!(r#"{{"type":"dns","value":{}}}"#, stringify(dns_name)?)); payload += &(format!(r#"{{"type":"dns","value":{}}}"#, stringify(dns_name)?));
if let Some(next) = iter.next() { if let Some(next) = iter.next() {
dns_name = &(next); dns_name = &(next);
payload.push(','); payload.push(',');
} else { } else {
break; 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");
} }
} }
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_path = args_iter.next().ok_or("expected path to cert file")?;
let (cert_kp, pkey_pem) = if let Some(pkey_pem) = pem_borrow.as_ref() { let pkey_path = args_iter.next().ok_or("expected path to pkey file")?;
let cert_kp = if let Some(passphrase) = &(pkey_passphrase) {
PKey::private_key_from_pem_passphrase(&(pkey_pem), passphrase.as_os_str().as_bytes()) 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 { } else {
PKey::private_key_from_pem(&(pkey_pem)) let cert_kp = gen_keypair()?;
}.map_err(|_| "failed to decode certificate keypair pem")?;
(cert_kp, pkey_pem) drop(pem_borrow);
} else { config_file.set_pkey_pem(privkey_to_pem(&(cert_kp), pkey_passphrase)?);
let cert_kp = Rsa::generate(2048).and_then(|keypair| PKey::from_rsa(keypair)).map_err(|_| "failed to generate rsa keypair")?;
drop(pem_borrow); pem_borrow = config_file.pkey_pem();
config_file.set_pkey_pem( (cert_kp, pem_borrow.as_ref().unwrap())
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 { let pkey_pem_view = from_utf8(&(pkey_pem)).map_err(|_| "invalid utf-8 bytes in pem-encoded private key")?;
cert_kp.private_key_to_pem_pkcs8()
}.map_err(|_| "failed to serialize 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(); let acme_order = loop {
(cert_kp, pem_borrow.as_ref().unwrap()) 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( if let Err(_) = cert_file.write(&(cert)) {
&(context), &(order.finalize), eprintln!("failed to write certificate to file, printing to stdout instead");
println!("{}", cert);
format!( }
r#"{{ "csr": {} }}"#, if let Err(_) = pkey_file.write(&*(pkey_pem)) {
stringify(b64ue_csr(gen_csr(&(cert_kp), &(order.dns_names))?)?)? eprintln!("failed to write private key to file, printing to stdout instead");
) println!("{}", pkey_pem_view);
)?;
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");
}
} }
std::thread::sleep(std::time::Duration::from_secs(3));
};
let cert = acme_post(&(context), &(acme_order.certificate.ok_or("expected acme certificate")?), NO_PAYLOAD)? drop(borrow);
.text() drop(pem_borrow);
.map_err(|_| "failed to read response")?; config_file.discard_order();
return Ok(());
if let Err(_) = cert_file.write(&(cert)) { },
eprintln!("failed to write certificate to file, printing to stdout instead"); _ => return Err("valid subactions for order: place, finalize"),
println!("{}", cert); }
} }
if let Err(_) = pkey_file.write(&*(pkey_pem)) { other => match args_iter.next().as_deref() {
eprintln!("failed to write private key to file, printing to stdout instead"); Some("test") => {
println!("{}", pkey_pem_view); let (cert_path, key_path) = (config_path, other.to_owned());
}
let kp = gen_keypair()?;
drop(borrow); let priv_pem = privkey_to_pem(&(kp), pkey_passphrase)?;
drop(pem_borrow);
config_file.discard_order()?; let mut builder = X509Builder::new().map_err(|_| "X509Builder::new() failed")?;
return Ok(()); builder.set_pubkey(&(kp)).map_err(|_| "builder.set_pubkey(...) failed")?;
},
_ => return Err("valid subactions for order: place, finalize"), 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(()); return Ok(());