cleaned up more, added root cert generation functionality (for testing mostly), changed .gitignore
This commit is contained in:
parent
93274fc906
commit
57dc94af88
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
/target
|
||||
test.json
|
||||
|
||||
|
11
src/acme.rs
11
src/acme.rs
@ -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();
|
||||
}
|
||||
}
|
71
src/config_file/acmec_config.rs
Normal file
71
src/config_file/acmec_config.rs
Normal file
@ -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
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
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);
|
||||
}
|
||||
}
|
476
src/main.rs
476
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<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(());
|
||||
|
Loading…
Reference in New Issue
Block a user