mirror of https://github.com/breard-r/acmed.git
				
				
			
				
				  
				  Failed to extract signature
				  
				
			
		
		
		
	
				 59 changed files with 45 additions and 10243 deletions
			
			
		- 
					2297Cargo.lock
 - 
					42Cargo.toml
 - 
					24Makefile
 - 
					41acme_common/Cargo.toml
 - 
					23acme_common/build.rs
 - 
					93acme_common/src/crypto.rs
 - 
					81acme_common/src/crypto/jws_signature_algorithm.rs
 - 
					103acme_common/src/crypto/key_type.rs
 - 
					203acme_common/src/crypto/openssl_certificate.rs
 - 
					34acme_common/src/crypto/openssl_hash.rs
 - 
					343acme_common/src/crypto/openssl_keys.rs
 - 
					25acme_common/src/crypto/openssl_subject_attribute.rs
 - 
					27acme_common/src/crypto/openssl_version.rs
 - 
					133acme_common/src/error.rs
 - 
					76acme_common/src/lib.rs
 - 
					86acme_common/src/logs.rs
 - 
					5acme_common/src/tests.rs
 - 
					181acme_common/src/tests/certificate.rs
 - 
					411acme_common/src/tests/crypto_keys.rs
 - 
					344acme_common/src/tests/hash.rs
 - 
					42acme_common/src/tests/idna.rs
 - 
					62acme_common/src/tests/jws_signature_algorithm.rs
 - 
					46acmed/Cargo.toml
 - 
					130acmed/build.rs
 - 
					319acmed/src/account.rs
 - 
					110acmed/src/account/contact.rs
 - 
					186acmed/src/account/storage.rs
 - 
					292acmed/src/acme_proto.rs
 - 
					154acmed/src/acme_proto/account.rs
 - 
					25acmed/src/acme_proto/certificate.rs
 - 
					149acmed/src/acme_proto/http.rs
 - 
					29acmed/src/acme_proto/structs.rs
 - 
					202acmed/src/acme_proto/structs/account.rs
 - 
					349acmed/src/acme_proto/structs/authorization.rs
 - 
					151acmed/src/acme_proto/structs/directory.rs
 - 
					173acmed/src/acme_proto/structs/error.rs
 - 
					135acmed/src/acme_proto/structs/order.rs
 - 
					203acmed/src/certificate.rs
 - 
					797acmed/src/config.rs
 - 
					51acmed/src/duration.rs
 - 
					130acmed/src/endpoint.rs
 - 
					215acmed/src/hooks.rs
 - 
					288acmed/src/http.rs
 - 
					147acmed/src/identifier.rs
 - 
					191acmed/src/jws.rs
 - 
					6acmed/src/logs.rs
 - 
					180acmed/src/main.rs
 - 
					223acmed/src/main_event_loop.rs
 - 
					312acmed/src/storage.rs
 - 
					60acmed/src/template.rs
 - 
					0config/acmed.toml
 - 
					0config/default_hooks.toml
 - 
					0config/letsencrypt.toml
 - 
					18release.sh
 - 
					1src/main.rs
 - 
					28tacd/Cargo.toml
 - 
					40tacd/build.rs
 - 
					224tacd/src/main.rs
 - 
					48tacd/src/openssl_server.rs
 
						
							
						
						
							2297
	
						
						Cargo.lock
						
							File diff suppressed because it is too large
							
							
								
									View File
								
							
						
					
				File diff suppressed because it is too large
							
							
								
									View File
								
							
						@ -1,11 +1,37 @@ | 
				
			|||
[workspace] | 
				
			|||
members = [ | 
				
			|||
    "acmed", | 
				
			|||
    "tacd", | 
				
			|||
] | 
				
			|||
[package] | 
				
			|||
name = "acmed" | 
				
			|||
version = "0.25.0-dev" | 
				
			|||
authors = ["Rodolphe Bréard <rodolphe@what.tf>"] | 
				
			|||
edition = "2021" | 
				
			|||
description = "ACME (RFC 8555) client daemon" | 
				
			|||
readme = "README.md" | 
				
			|||
repository = "https://github.com/breard-r/acmed" | 
				
			|||
license = "MIT OR Apache-2.0" | 
				
			|||
keywords = ["acme", "tls", "X.509"] | 
				
			|||
categories = ["cryptography"] | 
				
			|||
include = ["src/**/*", "Cargo.toml", "LICENSE-*.txt"] | 
				
			|||
publish = false | 
				
			|||
rust-version = "1.74.0" | 
				
			|||
 | 
				
			|||
[features] | 
				
			|||
default = ["openssl_dyn"] | 
				
			|||
crypto_openssl = [] | 
				
			|||
openssl_dyn = ["crypto_openssl"] | 
				
			|||
openssl_vendored = ["crypto_openssl"] | 
				
			|||
ed25519 = [] | 
				
			|||
ed448 = [] | 
				
			|||
 | 
				
			|||
[dependencies] | 
				
			|||
 | 
				
			|||
[target.'cfg(unix)'.dependencies] | 
				
			|||
 | 
				
			|||
[build-dependencies] | 
				
			|||
 | 
				
			|||
[profile.release] | 
				
			|||
opt-level = 'z' | 
				
			|||
lto = 'thin' | 
				
			|||
opt-level = "z" | 
				
			|||
debug = false | 
				
			|||
lto = true | 
				
			|||
codegen-units = 1 | 
				
			|||
panic = 'abort' | 
				
			|||
panic = "abort" | 
				
			|||
strip = true | 
				
			|||
incremental = false | 
				
			|||
@ -1,41 +0,0 @@ | 
				
			|||
[package] | 
				
			|||
name = "acme_common" | 
				
			|||
version = "0.24.0" | 
				
			|||
authors = ["Rodolphe Breard <rodolphe@what.tf>"] | 
				
			|||
edition = "2018" | 
				
			|||
readme = "../README.md" | 
				
			|||
repository = "https://github.com/breard-r/libreauth" | 
				
			|||
license = "MIT OR Apache-2.0" | 
				
			|||
include = ["src/**/*", "Cargo.toml", "Licence_*.txt"] | 
				
			|||
publish = false | 
				
			|||
rust-version = "1.74.0" | 
				
			|||
 | 
				
			|||
[lib] | 
				
			|||
name = "acme_common" | 
				
			|||
 | 
				
			|||
[features] | 
				
			|||
default = [] | 
				
			|||
crypto_openssl = [] | 
				
			|||
openssl_dyn = ["crypto_openssl", "openssl", "openssl-sys"] | 
				
			|||
openssl_vendored = ["crypto_openssl", "openssl/vendored", "openssl-sys/vendored"] | 
				
			|||
ed25519 = [] | 
				
			|||
ed448 = [] | 
				
			|||
 | 
				
			|||
[dependencies] | 
				
			|||
base64 = "0.22.0" | 
				
			|||
daemonize = "0.5.0" | 
				
			|||
env_logger = "0.11.3" | 
				
			|||
glob = "0.3.1" | 
				
			|||
log = "0.4.21" | 
				
			|||
minijinja = "2.5.0" | 
				
			|||
native-tls = "0.2.11" | 
				
			|||
openssl = { version = "0.10.64", optional = true } | 
				
			|||
openssl-sys = { version = "0.9.101", optional = true } | 
				
			|||
punycode = "0.4.1" | 
				
			|||
reqwest = { version = "0.12.1", default-features = false } | 
				
			|||
serde_json = "1.0.114" | 
				
			|||
syslog = "7.0.0" | 
				
			|||
toml = "0.8.12" | 
				
			|||
 | 
				
			|||
[target.'cfg(unix)'.dependencies] | 
				
			|||
nix = "0.29.0" | 
				
			|||
@ -1,23 +0,0 @@ | 
				
			|||
use std::env;
 | 
				
			|||
 | 
				
			|||
macro_rules! set_rustc_env_var {
 | 
				
			|||
	($name: expr, $value: expr) => {{
 | 
				
			|||
		println!("cargo:rustc-env={}={}", $name, $value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[allow(clippy::unusual_byte_groupings)]
 | 
				
			|||
fn main() {
 | 
				
			|||
	if let Ok(v) = env::var("DEP_OPENSSL_VERSION_NUMBER") {
 | 
				
			|||
		let version = u64::from_str_radix(&v, 16).unwrap();
 | 
				
			|||
		// OpenSSL 1.1.1
 | 
				
			|||
		if version >= 0x1_01_01_00_0 {
 | 
				
			|||
			println!("cargo:rustc-cfg=feature=\"ed25519\"");
 | 
				
			|||
			println!("cargo:rustc-cfg=feature=\"ed448\"");
 | 
				
			|||
		}
 | 
				
			|||
		set_rustc_env_var!("ACMED_TLS_LIB_NAME", "OpenSSL");
 | 
				
			|||
	}
 | 
				
			|||
	if env::var("DEP_OPENSSL_LIBRESSL_VERSION_NUMBER").is_ok() {
 | 
				
			|||
		set_rustc_env_var!("ACMED_TLS_LIB_NAME", "LibreSSL");
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,93 +0,0 @@ | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
mod jws_signature_algorithm;
 | 
				
			|||
mod key_type;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
mod openssl_certificate;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
mod openssl_hash;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
mod openssl_keys;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
mod openssl_subject_attribute;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
mod openssl_version;
 | 
				
			|||
 | 
				
			|||
const APP_ORG: &str = "ACMEd";
 | 
				
			|||
const APP_NAME: &str = "ACMEd";
 | 
				
			|||
const X509_VERSION: i32 = 0x02;
 | 
				
			|||
const CRT_SERIAL_NB_BITS: i32 = 32;
 | 
				
			|||
const INVALID_EXT_MSG: &str = "invalid acmeIdentifier extension";
 | 
				
			|||
pub const CRT_NB_DAYS_VALIDITY: u32 = 7;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
 | 
				
			|||
pub enum BaseSubjectAttribute {
 | 
				
			|||
	CountryName,
 | 
				
			|||
	GenerationQualifier,
 | 
				
			|||
	GivenName,
 | 
				
			|||
	Initials,
 | 
				
			|||
	LocalityName,
 | 
				
			|||
	Name,
 | 
				
			|||
	OrganizationName,
 | 
				
			|||
	OrganizationalUnitName,
 | 
				
			|||
	Pkcs9EmailAddress,
 | 
				
			|||
	PostalAddress,
 | 
				
			|||
	PostalCode,
 | 
				
			|||
	StateOrProvinceName,
 | 
				
			|||
	Street,
 | 
				
			|||
	Surname,
 | 
				
			|||
	Title,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Copy, Debug, PartialEq)]
 | 
				
			|||
pub enum BaseHashFunction {
 | 
				
			|||
	Sha256,
 | 
				
			|||
	Sha384,
 | 
				
			|||
	Sha512,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl BaseHashFunction {
 | 
				
			|||
	pub fn list_possible_values() -> Vec<&'static str> {
 | 
				
			|||
		vec!["sha256", "sha384", "sha512"]
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl FromStr for BaseHashFunction {
 | 
				
			|||
	type Err = Error;
 | 
				
			|||
 | 
				
			|||
	fn from_str(s: &str) -> Result<Self, Error> {
 | 
				
			|||
		let s = s.to_lowercase().replace(['-', '_'], "");
 | 
				
			|||
		match s.as_str() {
 | 
				
			|||
			"sha256" => Ok(BaseHashFunction::Sha256),
 | 
				
			|||
			"sha384" => Ok(BaseHashFunction::Sha384),
 | 
				
			|||
			"sha512" => Ok(BaseHashFunction::Sha512),
 | 
				
			|||
			_ => Err(format!("{s}: unknown hash function.").into()),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for BaseHashFunction {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			BaseHashFunction::Sha256 => "sha256",
 | 
				
			|||
			BaseHashFunction::Sha384 => "sha384",
 | 
				
			|||
			BaseHashFunction::Sha512 => "sha512",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub use jws_signature_algorithm::JwsSignatureAlgorithm;
 | 
				
			|||
pub use key_type::KeyType;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
pub use openssl_certificate::{Csr, X509Certificate};
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
pub use openssl_hash::HashFunction;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
pub use openssl_keys::{gen_keypair, KeyPair};
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
pub use openssl_subject_attribute::SubjectAttribute;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
pub use openssl_version::{get_lib_name, get_lib_version};
 | 
				
			|||
@ -1,81 +0,0 @@ | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Copy, Debug, PartialEq)]
 | 
				
			|||
pub enum JwsSignatureAlgorithm {
 | 
				
			|||
	Hs256,
 | 
				
			|||
	Hs384,
 | 
				
			|||
	Hs512,
 | 
				
			|||
	Rs256,
 | 
				
			|||
	Es256,
 | 
				
			|||
	Es384,
 | 
				
			|||
	Es512,
 | 
				
			|||
	#[cfg(feature = "ed25519")]
 | 
				
			|||
	Ed25519,
 | 
				
			|||
	#[cfg(feature = "ed448")]
 | 
				
			|||
	Ed448,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl FromStr for JwsSignatureAlgorithm {
 | 
				
			|||
	type Err = Error;
 | 
				
			|||
 | 
				
			|||
	fn from_str(s: &str) -> Result<Self, Error> {
 | 
				
			|||
		match s.to_lowercase().as_str() {
 | 
				
			|||
			"hs256" => Ok(JwsSignatureAlgorithm::Hs256),
 | 
				
			|||
			"hs384" => Ok(JwsSignatureAlgorithm::Hs384),
 | 
				
			|||
			"hs512" => Ok(JwsSignatureAlgorithm::Hs512),
 | 
				
			|||
			"rs256" => Ok(JwsSignatureAlgorithm::Rs256),
 | 
				
			|||
			"es256" => Ok(JwsSignatureAlgorithm::Es256),
 | 
				
			|||
			"es384" => Ok(JwsSignatureAlgorithm::Es384),
 | 
				
			|||
			"es512" => Ok(JwsSignatureAlgorithm::Es512),
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			"ed25519" => Ok(JwsSignatureAlgorithm::Ed25519),
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			"ed448" => Ok(JwsSignatureAlgorithm::Ed448),
 | 
				
			|||
			_ => Err(format!("{s}: unknown algorithm.").into()),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for JwsSignatureAlgorithm {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			JwsSignatureAlgorithm::Hs256 => "HS256",
 | 
				
			|||
			JwsSignatureAlgorithm::Hs384 => "HS384",
 | 
				
			|||
			JwsSignatureAlgorithm::Hs512 => "HS512",
 | 
				
			|||
			JwsSignatureAlgorithm::Rs256 => "RS256",
 | 
				
			|||
			JwsSignatureAlgorithm::Es256 => "ES256",
 | 
				
			|||
			JwsSignatureAlgorithm::Es384 => "ES384",
 | 
				
			|||
			JwsSignatureAlgorithm::Es512 => "ES512",
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			JwsSignatureAlgorithm::Ed25519 => "Ed25519",
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			JwsSignatureAlgorithm::Ed448 => "Ed448",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::JwsSignatureAlgorithm;
 | 
				
			|||
	use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_es256_from_str() {
 | 
				
			|||
		let variants = ["ES256", "Es256", "es256"];
 | 
				
			|||
		for v in variants.iter() {
 | 
				
			|||
			let a = JwsSignatureAlgorithm::from_str(v);
 | 
				
			|||
			assert!(a.is_ok());
 | 
				
			|||
			let a = a.unwrap();
 | 
				
			|||
			assert_eq!(a, JwsSignatureAlgorithm::Es256);
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_es256_to_str() {
 | 
				
			|||
		let a = JwsSignatureAlgorithm::Es256;
 | 
				
			|||
		assert_eq!(a.to_string().as_str(), "ES256");
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,103 +0,0 @@ | 
				
			|||
use crate::crypto::JwsSignatureAlgorithm;
 | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Copy, Debug, PartialEq)]
 | 
				
			|||
pub enum KeyType {
 | 
				
			|||
	Rsa2048,
 | 
				
			|||
	Rsa4096,
 | 
				
			|||
	EcdsaP256,
 | 
				
			|||
	EcdsaP384,
 | 
				
			|||
	EcdsaP521,
 | 
				
			|||
	#[cfg(feature = "ed25519")]
 | 
				
			|||
	Ed25519,
 | 
				
			|||
	#[cfg(feature = "ed448")]
 | 
				
			|||
	Ed448,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl KeyType {
 | 
				
			|||
	pub fn get_default_signature_alg(&self) -> JwsSignatureAlgorithm {
 | 
				
			|||
		match self {
 | 
				
			|||
			KeyType::Rsa2048 | KeyType::Rsa4096 => JwsSignatureAlgorithm::Rs256,
 | 
				
			|||
			KeyType::EcdsaP256 => JwsSignatureAlgorithm::Es256,
 | 
				
			|||
			KeyType::EcdsaP384 => JwsSignatureAlgorithm::Es384,
 | 
				
			|||
			KeyType::EcdsaP521 => JwsSignatureAlgorithm::Es512,
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			KeyType::Ed25519 => JwsSignatureAlgorithm::Ed25519,
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			KeyType::Ed448 => JwsSignatureAlgorithm::Ed448,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn check_alg_compatibility(&self, alg: &JwsSignatureAlgorithm) -> Result<(), Error> {
 | 
				
			|||
		let ok = match self {
 | 
				
			|||
			KeyType::Rsa2048 | KeyType::Rsa4096 => *alg == JwsSignatureAlgorithm::Rs256,
 | 
				
			|||
			KeyType::EcdsaP256 | KeyType::EcdsaP384 | KeyType::EcdsaP521 => {
 | 
				
			|||
				*alg == self.get_default_signature_alg()
 | 
				
			|||
			}
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			KeyType::Ed25519 => *alg == self.get_default_signature_alg(),
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			KeyType::Ed448 => *alg == self.get_default_signature_alg(),
 | 
				
			|||
		};
 | 
				
			|||
		if ok {
 | 
				
			|||
			Ok(())
 | 
				
			|||
		} else {
 | 
				
			|||
			let err_msg = format!(
 | 
				
			|||
				"incompatible signature algorithm: {alg} cannot be used with an {self} key"
 | 
				
			|||
			);
 | 
				
			|||
			Err(err_msg.into())
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn list_possible_values() -> Vec<&'static str> {
 | 
				
			|||
		vec![
 | 
				
			|||
			"rsa2048",
 | 
				
			|||
			"rsa4096",
 | 
				
			|||
			"ecdsa-p256",
 | 
				
			|||
			"ecdsa-p384",
 | 
				
			|||
			"ecdsa-p521",
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			"ed25519",
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			"ed448",
 | 
				
			|||
		]
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl FromStr for KeyType {
 | 
				
			|||
	type Err = Error;
 | 
				
			|||
 | 
				
			|||
	fn from_str(s: &str) -> Result<Self, Error> {
 | 
				
			|||
		match s.to_lowercase().replace('-', "_").as_str() {
 | 
				
			|||
			"rsa2048" => Ok(KeyType::Rsa2048),
 | 
				
			|||
			"rsa4096" => Ok(KeyType::Rsa4096),
 | 
				
			|||
			"ecdsa_p256" => Ok(KeyType::EcdsaP256),
 | 
				
			|||
			"ecdsa_p384" => Ok(KeyType::EcdsaP384),
 | 
				
			|||
			"ecdsa_p521" => Ok(KeyType::EcdsaP521),
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			"ed25519" => Ok(KeyType::Ed25519),
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			"ed448" => Ok(KeyType::Ed448),
 | 
				
			|||
			_ => Err(format!("{s}: unknown algorithm").into()),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for KeyType {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			KeyType::Rsa2048 => "rsa2048",
 | 
				
			|||
			KeyType::Rsa4096 => "rsa4096",
 | 
				
			|||
			KeyType::EcdsaP256 => "ecdsa-p256",
 | 
				
			|||
			KeyType::EcdsaP384 => "ecdsa-p384",
 | 
				
			|||
			KeyType::EcdsaP521 => "ecdsa-p521",
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			KeyType::Ed25519 => "ed25519",
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			KeyType::Ed448 => "ed448",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,203 +0,0 @@ | 
				
			|||
use super::{gen_keypair, KeyPair, KeyType, SubjectAttribute};
 | 
				
			|||
use crate::b64_encode;
 | 
				
			|||
use crate::crypto::HashFunction;
 | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use openssl::asn1::Asn1Time;
 | 
				
			|||
use openssl::bn::{BigNum, MsbOption};
 | 
				
			|||
use openssl::hash::MessageDigest;
 | 
				
			|||
use openssl::stack::Stack;
 | 
				
			|||
use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
 | 
				
			|||
use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509};
 | 
				
			|||
use std::collections::{HashMap, HashSet};
 | 
				
			|||
use std::net::IpAddr;
 | 
				
			|||
use std::time::Duration;
 | 
				
			|||
 | 
				
			|||
fn get_digest(digest: HashFunction, key_pair: &KeyPair) -> MessageDigest {
 | 
				
			|||
	#[cfg(not(any(feature = "ed25519", feature = "ed448")))]
 | 
				
			|||
	let digest = digest.native_digest();
 | 
				
			|||
	let _ = key_pair;
 | 
				
			|||
	#[cfg(any(feature = "ed25519", feature = "ed448"))]
 | 
				
			|||
	let digest = match key_pair.key_type {
 | 
				
			|||
		#[cfg(feature = "ed25519")]
 | 
				
			|||
		KeyType::Ed25519 => MessageDigest::null(),
 | 
				
			|||
		#[cfg(feature = "ed448")]
 | 
				
			|||
		KeyType::Ed448 => MessageDigest::null(),
 | 
				
			|||
		_ => digest.native_digest(),
 | 
				
			|||
	};
 | 
				
			|||
	digest
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub struct Csr {
 | 
				
			|||
	inner_csr: X509Req,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Csr {
 | 
				
			|||
	pub fn new(
 | 
				
			|||
		key_pair: &KeyPair,
 | 
				
			|||
		digest: HashFunction,
 | 
				
			|||
		domains: &[String],
 | 
				
			|||
		ips: &[String],
 | 
				
			|||
		subject_attributes: &HashMap<SubjectAttribute, String>,
 | 
				
			|||
	) -> Result<Self, Error> {
 | 
				
			|||
		let mut builder = X509ReqBuilder::new()?;
 | 
				
			|||
		builder.set_pubkey(&key_pair.inner_key)?;
 | 
				
			|||
		if !subject_attributes.is_empty() {
 | 
				
			|||
			let mut snb = X509NameBuilder::new()?;
 | 
				
			|||
			for (sattr, val) in subject_attributes.iter() {
 | 
				
			|||
				snb.append_entry_by_nid(sattr.get_nid(), val)?;
 | 
				
			|||
			}
 | 
				
			|||
			let name = snb.build();
 | 
				
			|||
			builder.set_subject_name(&name)?;
 | 
				
			|||
		}
 | 
				
			|||
		let ctx = builder.x509v3_context(None);
 | 
				
			|||
		let mut san = SubjectAlternativeName::new();
 | 
				
			|||
		for dns in domains.iter() {
 | 
				
			|||
			san.dns(dns);
 | 
				
			|||
		}
 | 
				
			|||
		for ip in ips.iter() {
 | 
				
			|||
			san.ip(ip);
 | 
				
			|||
		}
 | 
				
			|||
		let san = san.build(&ctx)?;
 | 
				
			|||
		let mut ext_stack = Stack::new()?;
 | 
				
			|||
		ext_stack.push(san)?;
 | 
				
			|||
		builder.add_extensions(&ext_stack)?;
 | 
				
			|||
		let digest = get_digest(digest, key_pair);
 | 
				
			|||
		builder.sign(&key_pair.inner_key, digest)?;
 | 
				
			|||
		Ok(Csr {
 | 
				
			|||
			inner_csr: builder.build(),
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn to_der_base64(&self) -> Result<String, Error> {
 | 
				
			|||
		let csr = self.inner_csr.to_der()?;
 | 
				
			|||
		let csr = b64_encode(&csr);
 | 
				
			|||
		Ok(csr)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn to_pem(&self) -> Result<String, Error> {
 | 
				
			|||
		let csr = self.inner_csr.to_pem()?;
 | 
				
			|||
		Ok(String::from_utf8(csr)?)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub struct X509Certificate {
 | 
				
			|||
	pub inner_cert: X509,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl X509Certificate {
 | 
				
			|||
	pub fn from_pem(pem_data: &[u8]) -> Result<Self, Error> {
 | 
				
			|||
		Ok(X509Certificate {
 | 
				
			|||
			inner_cert: X509::from_pem(pem_data)?,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn from_pem_native(pem_data: &[u8]) -> Result<native_tls::Certificate, Error> {
 | 
				
			|||
		Ok(native_tls::Certificate::from_pem(pem_data)?)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn from_acme_ext(
 | 
				
			|||
		domain: &str,
 | 
				
			|||
		acme_ext: &str,
 | 
				
			|||
		key_type: KeyType,
 | 
				
			|||
		digest: HashFunction,
 | 
				
			|||
	) -> Result<(KeyPair, Self), Error> {
 | 
				
			|||
		let key_pair = gen_keypair(key_type)?;
 | 
				
			|||
		let digest = get_digest(digest, &key_pair);
 | 
				
			|||
		let inner_cert = gen_certificate(domain, &key_pair, &digest, acme_ext)?;
 | 
				
			|||
		let cert = X509Certificate { inner_cert };
 | 
				
			|||
		Ok((key_pair, cert))
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn expires_in(&self) -> Result<Duration, Error> {
 | 
				
			|||
		let now = Asn1Time::days_from_now(0)?;
 | 
				
			|||
		let not_after = self.inner_cert.not_after();
 | 
				
			|||
		let diff = now.diff(not_after)?;
 | 
				
			|||
		let nb_secs = diff.days * 24 * 60 * 60 + diff.secs;
 | 
				
			|||
		let nb_secs = if nb_secs > 0 { nb_secs as u64 } else { 0 };
 | 
				
			|||
		Ok(Duration::from_secs(nb_secs))
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn subject_alt_names(&self) -> HashSet<String> {
 | 
				
			|||
		match self.inner_cert.subject_alt_names() {
 | 
				
			|||
			Some(s) => s
 | 
				
			|||
				.iter()
 | 
				
			|||
				.filter(|v| v.dnsname().is_some() || v.ipaddress().is_some())
 | 
				
			|||
				.map(|v| match v.dnsname() {
 | 
				
			|||
					Some(d) => d.to_string(),
 | 
				
			|||
					None => match v.ipaddress() {
 | 
				
			|||
						Some(i) => match i.len() {
 | 
				
			|||
							4 => {
 | 
				
			|||
								let ipv4: [u8; 4] = [i[0], i[1], i[2], i[3]];
 | 
				
			|||
								IpAddr::from(ipv4).to_string()
 | 
				
			|||
							}
 | 
				
			|||
							16 => {
 | 
				
			|||
								let ipv6: [u8; 16] = [
 | 
				
			|||
									i[0], i[1], i[2], i[3], i[4], i[5], i[6], i[7], i[8], i[9],
 | 
				
			|||
									i[10], i[11], i[12], i[13], i[14], i[15],
 | 
				
			|||
								];
 | 
				
			|||
								IpAddr::from(ipv6).to_string()
 | 
				
			|||
							}
 | 
				
			|||
							_ => String::new(),
 | 
				
			|||
						},
 | 
				
			|||
						None => String::new(),
 | 
				
			|||
					},
 | 
				
			|||
				})
 | 
				
			|||
				.collect(),
 | 
				
			|||
			None => HashSet::new(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn gen_certificate(
 | 
				
			|||
	domain: &str,
 | 
				
			|||
	key_pair: &KeyPair,
 | 
				
			|||
	digest: &MessageDigest,
 | 
				
			|||
	acme_ext: &str,
 | 
				
			|||
) -> Result<X509, Error> {
 | 
				
			|||
	let mut x509_name = X509NameBuilder::new()?;
 | 
				
			|||
	x509_name.append_entry_by_text("O", super::APP_ORG)?;
 | 
				
			|||
	let ca_name = format!("{} TLS-ALPN-01 Authority", super::APP_NAME);
 | 
				
			|||
	x509_name.append_entry_by_text("CN", &ca_name)?;
 | 
				
			|||
	let x509_name = x509_name.build();
 | 
				
			|||
 | 
				
			|||
	let mut builder = X509Builder::new()?;
 | 
				
			|||
	builder.set_version(super::X509_VERSION)?;
 | 
				
			|||
	let serial_number = {
 | 
				
			|||
		let mut serial = BigNum::new()?;
 | 
				
			|||
		serial.rand(super::CRT_SERIAL_NB_BITS - 1, MsbOption::MAYBE_ZERO, false)?;
 | 
				
			|||
		serial.to_asn1_integer()?
 | 
				
			|||
	};
 | 
				
			|||
	builder.set_serial_number(&serial_number)?;
 | 
				
			|||
	builder.set_subject_name(&x509_name)?;
 | 
				
			|||
	builder.set_issuer_name(&x509_name)?;
 | 
				
			|||
	builder.set_pubkey(&key_pair.inner_key)?;
 | 
				
			|||
	let not_before = Asn1Time::days_from_now(0)?;
 | 
				
			|||
	builder.set_not_before(¬_before)?;
 | 
				
			|||
	let not_after = Asn1Time::days_from_now(super::CRT_NB_DAYS_VALIDITY)?;
 | 
				
			|||
	builder.set_not_after(¬_after)?;
 | 
				
			|||
 | 
				
			|||
	builder.append_extension(BasicConstraints::new().build()?)?;
 | 
				
			|||
	let ctx = builder.x509v3_context(None, None);
 | 
				
			|||
	let san_ext = SubjectAlternativeName::new().dns(domain).build(&ctx)?;
 | 
				
			|||
	builder.append_extension(san_ext)?;
 | 
				
			|||
 | 
				
			|||
	if !acme_ext.is_empty() {
 | 
				
			|||
		let ctx = builder.x509v3_context(None, None);
 | 
				
			|||
		let mut v: Vec<&str> = acme_ext.split('=').collect();
 | 
				
			|||
		let value = v.pop().ok_or_else(|| Error::from(super::INVALID_EXT_MSG))?;
 | 
				
			|||
		let acme_ext_name = v.pop().ok_or_else(|| Error::from(super::INVALID_EXT_MSG))?;
 | 
				
			|||
		if !v.is_empty() {
 | 
				
			|||
			return Err(Error::from(super::INVALID_EXT_MSG));
 | 
				
			|||
		}
 | 
				
			|||
		#[allow(deprecated)]
 | 
				
			|||
		let acme_ext = X509Extension::new(None, Some(&ctx), acme_ext_name, value)
 | 
				
			|||
			.map_err(|_| Error::from(super::INVALID_EXT_MSG))?;
 | 
				
			|||
		builder
 | 
				
			|||
			.append_extension(acme_ext)
 | 
				
			|||
			.map_err(|_| Error::from(super::INVALID_EXT_MSG))?;
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	builder.sign(&key_pair.inner_key, *digest)?;
 | 
				
			|||
	let cert = builder.build();
 | 
				
			|||
	Ok(cert)
 | 
				
			|||
}
 | 
				
			|||
@ -1,34 +0,0 @@ | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use openssl::hash::MessageDigest;
 | 
				
			|||
use openssl::pkey::PKey;
 | 
				
			|||
use openssl::sha::{sha256, sha384, sha512};
 | 
				
			|||
use openssl::sign::Signer;
 | 
				
			|||
 | 
				
			|||
pub type HashFunction = super::BaseHashFunction;
 | 
				
			|||
 | 
				
			|||
impl HashFunction {
 | 
				
			|||
	pub fn hash(&self, data: &[u8]) -> Vec<u8> {
 | 
				
			|||
		match self {
 | 
				
			|||
			HashFunction::Sha256 => sha256(data).to_vec(),
 | 
				
			|||
			HashFunction::Sha384 => sha384(data).to_vec(),
 | 
				
			|||
			HashFunction::Sha512 => sha512(data).to_vec(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn hmac(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		let key = PKey::hmac(key)?;
 | 
				
			|||
		let h_func = self.native_digest();
 | 
				
			|||
		let mut signer = Signer::new(h_func, &key)?;
 | 
				
			|||
		signer.update(data)?;
 | 
				
			|||
		let res = signer.sign_to_vec()?;
 | 
				
			|||
		Ok(res)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub(crate) fn native_digest(&self) -> MessageDigest {
 | 
				
			|||
		match self {
 | 
				
			|||
			HashFunction::Sha256 => MessageDigest::sha256(),
 | 
				
			|||
			HashFunction::Sha384 => MessageDigest::sha384(),
 | 
				
			|||
			HashFunction::Sha512 => MessageDigest::sha512(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,343 +0,0 @@ | 
				
			|||
use crate::b64_encode;
 | 
				
			|||
use crate::crypto::{HashFunction, JwsSignatureAlgorithm, KeyType};
 | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use openssl::bn::{BigNum, BigNumContext};
 | 
				
			|||
use openssl::ec::{Asn1Flag, EcGroup, EcKey};
 | 
				
			|||
use openssl::ecdsa::EcdsaSig;
 | 
				
			|||
use openssl::hash::MessageDigest;
 | 
				
			|||
use openssl::nid::Nid;
 | 
				
			|||
use openssl::pkey::{Id, PKey, Private};
 | 
				
			|||
use openssl::rsa::Rsa;
 | 
				
			|||
use openssl::sign::Signer;
 | 
				
			|||
use serde_json::json;
 | 
				
			|||
use serde_json::value::Value;
 | 
				
			|||
 | 
				
			|||
macro_rules! get_key_type {
 | 
				
			|||
	($key: expr) => {
 | 
				
			|||
		match $key.id() {
 | 
				
			|||
			Id::RSA => match $key.rsa()?.size() {
 | 
				
			|||
				256 => KeyType::Rsa2048,
 | 
				
			|||
				512 => KeyType::Rsa4096,
 | 
				
			|||
				s => {
 | 
				
			|||
					return Err(format!("{}: unsupported RSA key size", s * 8).into());
 | 
				
			|||
				}
 | 
				
			|||
			},
 | 
				
			|||
			Id::EC => match $key.ec_key()?.group().curve_name() {
 | 
				
			|||
				Some(Nid::X9_62_PRIME256V1) => KeyType::EcdsaP256,
 | 
				
			|||
				Some(Nid::SECP384R1) => KeyType::EcdsaP384,
 | 
				
			|||
				Some(Nid::SECP521R1) => KeyType::EcdsaP521,
 | 
				
			|||
				Some(nid) => {
 | 
				
			|||
					return Err(format!("{:?}: unsupported EC key", nid).into());
 | 
				
			|||
				}
 | 
				
			|||
				None => {
 | 
				
			|||
					return Err("unsupported EC key".into());
 | 
				
			|||
				}
 | 
				
			|||
			},
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			Id::ED25519 => KeyType::Ed25519,
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			Id::ED448 => KeyType::Ed448,
 | 
				
			|||
			_ => {
 | 
				
			|||
				return Err("unsupported key type".into());
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! get_ecdsa_sig_part {
 | 
				
			|||
	($part: expr, $size: ident) => {{
 | 
				
			|||
		let mut p = $part.to_vec();
 | 
				
			|||
		let length = p.len();
 | 
				
			|||
		if length != $size {
 | 
				
			|||
			let mut s: Vec<u8> = Vec::with_capacity($size);
 | 
				
			|||
			s.resize_with($size - length, || 0);
 | 
				
			|||
			s.append(&mut p);
 | 
				
			|||
			s
 | 
				
			|||
		} else {
 | 
				
			|||
			p
 | 
				
			|||
		}
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct KeyPair {
 | 
				
			|||
	pub key_type: KeyType,
 | 
				
			|||
	pub inner_key: PKey<Private>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl KeyPair {
 | 
				
			|||
	pub fn from_der(der_data: &[u8]) -> Result<Self, Error> {
 | 
				
			|||
		let inner_key = PKey::private_key_from_der(der_data)?;
 | 
				
			|||
		let key_type = get_key_type!(inner_key);
 | 
				
			|||
		Ok(KeyPair {
 | 
				
			|||
			key_type,
 | 
				
			|||
			inner_key,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn from_pem(pem_data: &[u8]) -> Result<Self, Error> {
 | 
				
			|||
		let inner_key = PKey::private_key_from_pem(pem_data)?;
 | 
				
			|||
		let key_type = get_key_type!(inner_key);
 | 
				
			|||
		Ok(KeyPair {
 | 
				
			|||
			key_type,
 | 
				
			|||
			inner_key,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn private_key_to_der(&self) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		self.inner_key.private_key_to_der().map_err(Error::from)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn private_key_to_pem(&self) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		self.inner_key
 | 
				
			|||
			.private_key_to_pem_pkcs8()
 | 
				
			|||
			.map_err(Error::from)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn public_key_to_pem(&self) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		self.inner_key.public_key_to_pem().map_err(Error::from)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn sign(&self, alg: &JwsSignatureAlgorithm, data: &[u8]) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		self.key_type.check_alg_compatibility(alg)?;
 | 
				
			|||
		match alg {
 | 
				
			|||
			JwsSignatureAlgorithm::Hs256
 | 
				
			|||
			| JwsSignatureAlgorithm::Hs384
 | 
				
			|||
			| JwsSignatureAlgorithm::Hs512 => Err(format!(
 | 
				
			|||
				"{} key pair cannot be used for the {alg} signature algorithm",
 | 
				
			|||
				self.key_type
 | 
				
			|||
			)
 | 
				
			|||
			.into()),
 | 
				
			|||
			JwsSignatureAlgorithm::Rs256 => self.sign_rsa(&MessageDigest::sha256(), data),
 | 
				
			|||
			JwsSignatureAlgorithm::Es256 => self.sign_ecdsa(&HashFunction::Sha256, data),
 | 
				
			|||
			JwsSignatureAlgorithm::Es384 => self.sign_ecdsa(&HashFunction::Sha384, data),
 | 
				
			|||
			JwsSignatureAlgorithm::Es512 => self.sign_ecdsa(&HashFunction::Sha512, data),
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			JwsSignatureAlgorithm::Ed25519 => self.sign_eddsa(data),
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			JwsSignatureAlgorithm::Ed448 => self.sign_eddsa(data),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn sign_rsa(&self, hash_func: &MessageDigest, data: &[u8]) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		let mut signer = Signer::new(*hash_func, &self.inner_key)?;
 | 
				
			|||
		signer.update(data)?;
 | 
				
			|||
		let signature = signer.sign_to_vec()?;
 | 
				
			|||
		Ok(signature)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn sign_ecdsa(&self, hash_func: &HashFunction, data: &[u8]) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		let fingerprint = hash_func.hash(data);
 | 
				
			|||
		let signature = EcdsaSig::sign(&fingerprint, self.inner_key.ec_key()?.as_ref())?;
 | 
				
			|||
		let sig_size = match self.key_type {
 | 
				
			|||
			KeyType::EcdsaP256 => 32,
 | 
				
			|||
			KeyType::EcdsaP384 => 48,
 | 
				
			|||
			KeyType::EcdsaP521 => 66,
 | 
				
			|||
			_ => {
 | 
				
			|||
				return Err("not an ecdsa key".into());
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		let r = get_ecdsa_sig_part!(signature.r(), sig_size);
 | 
				
			|||
		let mut s = get_ecdsa_sig_part!(signature.s(), sig_size);
 | 
				
			|||
		let mut signature = r;
 | 
				
			|||
		signature.append(&mut s);
 | 
				
			|||
		Ok(signature)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[cfg(any(feature = "ed25519", feature = "ed448"))]
 | 
				
			|||
	fn sign_eddsa(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
 | 
				
			|||
		let mut signer = Signer::new_without_digest(&self.inner_key)?;
 | 
				
			|||
		let signature = signer.sign_oneshot_to_vec(data)?;
 | 
				
			|||
		Ok(signature)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn jwk_public_key(&self) -> Result<Value, Error> {
 | 
				
			|||
		self.get_jwk_public_key(false)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn jwk_public_key_thumbprint(&self) -> Result<Value, Error> {
 | 
				
			|||
		self.get_jwk_public_key(true)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn get_jwk_public_key(&self, thumbprint: bool) -> Result<Value, Error> {
 | 
				
			|||
		match self.key_type {
 | 
				
			|||
			KeyType::Rsa2048 | KeyType::Rsa4096 => self.get_rsa_jwk(thumbprint),
 | 
				
			|||
			KeyType::EcdsaP256 | KeyType::EcdsaP384 | KeyType::EcdsaP521 => {
 | 
				
			|||
				self.get_ecdsa_jwk(thumbprint)
 | 
				
			|||
			}
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			KeyType::Ed25519 => self.get_eddsa_jwk(thumbprint),
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			KeyType::Ed448 => self.get_eddsa_jwk(thumbprint),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn get_rsa_jwk(&self, thumbprint: bool) -> Result<Value, Error> {
 | 
				
			|||
		let rsa = self.inner_key.rsa().unwrap();
 | 
				
			|||
		let e = rsa.e();
 | 
				
			|||
		let n = rsa.n();
 | 
				
			|||
		let e = b64_encode(&e.to_vec());
 | 
				
			|||
		let n = b64_encode(&n.to_vec());
 | 
				
			|||
		let jwk = if thumbprint {
 | 
				
			|||
			json!({
 | 
				
			|||
				"kty": "RSA",
 | 
				
			|||
				"e": e,
 | 
				
			|||
				"n": n,
 | 
				
			|||
			})
 | 
				
			|||
		} else {
 | 
				
			|||
			json!({
 | 
				
			|||
				"alg": "RS256",
 | 
				
			|||
				"kty": "RSA",
 | 
				
			|||
				"use": "sig",
 | 
				
			|||
				"e": e,
 | 
				
			|||
				"n": n,
 | 
				
			|||
			})
 | 
				
			|||
		};
 | 
				
			|||
		Ok(jwk)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn get_ecdsa_jwk(&self, thumbprint: bool) -> Result<Value, Error> {
 | 
				
			|||
		let (crv, alg, size, curve) = match self.key_type {
 | 
				
			|||
			KeyType::EcdsaP256 => ("P-256", "ES256", 32, Nid::X9_62_PRIME256V1),
 | 
				
			|||
			KeyType::EcdsaP384 => ("P-384", "ES384", 48, Nid::SECP384R1),
 | 
				
			|||
			KeyType::EcdsaP521 => ("P-521", "ES512", 66, Nid::SECP521R1),
 | 
				
			|||
			_ => {
 | 
				
			|||
				return Err("not an ECDSA elliptic curve".into());
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		let group = EcGroup::from_curve_name(curve).unwrap();
 | 
				
			|||
		let mut ctx = BigNumContext::new().unwrap();
 | 
				
			|||
		let mut x = BigNum::new().unwrap();
 | 
				
			|||
		let mut y = BigNum::new().unwrap();
 | 
				
			|||
		self.inner_key
 | 
				
			|||
			.ec_key()
 | 
				
			|||
			.unwrap()
 | 
				
			|||
			.public_key()
 | 
				
			|||
			.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?;
 | 
				
			|||
		let x = b64_encode(&x.to_vec_padded(size)?);
 | 
				
			|||
		let y = b64_encode(&y.to_vec_padded(size)?);
 | 
				
			|||
		let jwk = if thumbprint {
 | 
				
			|||
			json!({
 | 
				
			|||
				"crv": crv,
 | 
				
			|||
				"kty": "EC",
 | 
				
			|||
				"x": x,
 | 
				
			|||
				"y": y,
 | 
				
			|||
			})
 | 
				
			|||
		} else {
 | 
				
			|||
			json!({
 | 
				
			|||
				"alg": alg,
 | 
				
			|||
				"crv": crv,
 | 
				
			|||
				"kty": "EC",
 | 
				
			|||
				"use": "sig",
 | 
				
			|||
				"x": x,
 | 
				
			|||
				"y": y,
 | 
				
			|||
			})
 | 
				
			|||
		};
 | 
				
			|||
		Ok(jwk)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[cfg(any(feature = "ed25519", feature = "ed448"))]
 | 
				
			|||
	fn get_eddsa_jwk(&self, thumbprint: bool) -> Result<Value, Error> {
 | 
				
			|||
		let crv = match self.key_type {
 | 
				
			|||
			#[cfg(feature = "ed25519")]
 | 
				
			|||
			KeyType::Ed25519 => "Ed25519",
 | 
				
			|||
			#[cfg(feature = "ed448")]
 | 
				
			|||
			KeyType::Ed448 => "Ed448",
 | 
				
			|||
			_ => {
 | 
				
			|||
				return Err("not an EdDSA elliptic curve".into());
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
 | 
				
			|||
		// /!\ WARNING: HAZARDOUS AND UGLY CODE /!\
 | 
				
			|||
		//
 | 
				
			|||
		// I couldn't find a way to get the value of `x` using the OpenSSL
 | 
				
			|||
		// interface, therefore I had to hack my way arround.
 | 
				
			|||
		//
 | 
				
			|||
		// The idea behind this hack is to export the public key in PEM, then
 | 
				
			|||
		// get the PEM base64 part, convert it to base64url without padding
 | 
				
			|||
		// and finally truncate the first part so only the value of `x`
 | 
				
			|||
		// remains.
 | 
				
			|||
 | 
				
			|||
		// -----BEGIN UGLY-----
 | 
				
			|||
		let mut x = String::new();
 | 
				
			|||
		let public_pem = self.public_key_to_pem()?;
 | 
				
			|||
		let public_pem = String::from_utf8(public_pem)?;
 | 
				
			|||
		for pem_line in public_pem.lines() {
 | 
				
			|||
			if !pem_line.is_empty() && !pem_line.starts_with("-----") {
 | 
				
			|||
				x += &pem_line
 | 
				
			|||
					.trim()
 | 
				
			|||
					.trim_end_matches('=')
 | 
				
			|||
					.replace('/', "_")
 | 
				
			|||
					.replace('+', "-");
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		x.replace_range(..16, "");
 | 
				
			|||
		// -----END UGLY-----
 | 
				
			|||
 | 
				
			|||
		let jwk = if thumbprint {
 | 
				
			|||
			json!({
 | 
				
			|||
				"crv": crv,
 | 
				
			|||
				"kty": "OKP",
 | 
				
			|||
				"x": &x,
 | 
				
			|||
			})
 | 
				
			|||
		} else {
 | 
				
			|||
			json!({
 | 
				
			|||
				"alg": "EdDSA",
 | 
				
			|||
				"crv": crv,
 | 
				
			|||
				"kty": "OKP",
 | 
				
			|||
				"use": "sig",
 | 
				
			|||
				"x": &x,
 | 
				
			|||
			})
 | 
				
			|||
		};
 | 
				
			|||
		Ok(jwk)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn gen_rsa_pair(nb_bits: u32) -> Result<PKey<Private>, Error> {
 | 
				
			|||
	let priv_key = Rsa::generate(nb_bits)?;
 | 
				
			|||
	let pk = PKey::from_rsa(priv_key).map_err(|_| Error::from(""))?;
 | 
				
			|||
	Ok(pk)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn gen_ec_pair(nid: Nid) -> Result<PKey<Private>, Error> {
 | 
				
			|||
	let mut group = EcGroup::from_curve_name(nid)?;
 | 
				
			|||
 | 
				
			|||
	// Use NAMED_CURVE format; OpenSSL 1.0.1 and 1.0.2 default to EXPLICIT_CURVE which won't work (see #9)
 | 
				
			|||
	group.set_asn1_flag(Asn1Flag::NAMED_CURVE);
 | 
				
			|||
 | 
				
			|||
	let ec_priv_key = EcKey::generate(&group).map_err(|_| Error::from(""))?;
 | 
				
			|||
	let pk = PKey::from_ec_key(ec_priv_key).map_err(|_| Error::from(""))?;
 | 
				
			|||
	Ok(pk)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
fn gen_ed25519_pair() -> Result<PKey<Private>, Error> {
 | 
				
			|||
	let pk = PKey::generate_ed25519().map_err(|_| Error::from(""))?;
 | 
				
			|||
	Ok(pk)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed448")]
 | 
				
			|||
fn gen_ed448_pair() -> Result<PKey<Private>, Error> {
 | 
				
			|||
	let pk = PKey::generate_ed448().map_err(|_| Error::from(""))?;
 | 
				
			|||
	Ok(pk)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn gen_keypair(key_type: KeyType) -> Result<KeyPair, Error> {
 | 
				
			|||
	let priv_key = match key_type {
 | 
				
			|||
		KeyType::Rsa2048 => gen_rsa_pair(2048),
 | 
				
			|||
		KeyType::Rsa4096 => gen_rsa_pair(4096),
 | 
				
			|||
		KeyType::EcdsaP256 => gen_ec_pair(Nid::X9_62_PRIME256V1),
 | 
				
			|||
		KeyType::EcdsaP384 => gen_ec_pair(Nid::SECP384R1),
 | 
				
			|||
		KeyType::EcdsaP521 => gen_ec_pair(Nid::SECP521R1),
 | 
				
			|||
		#[cfg(feature = "ed25519")]
 | 
				
			|||
		KeyType::Ed25519 => gen_ed25519_pair(),
 | 
				
			|||
		#[cfg(feature = "ed448")]
 | 
				
			|||
		KeyType::Ed448 => gen_ed448_pair(),
 | 
				
			|||
	}
 | 
				
			|||
	.map_err(|_| Error::from(format!("unable to generate a {key_type} key pair")))?;
 | 
				
			|||
	let key_pair = KeyPair {
 | 
				
			|||
		key_type,
 | 
				
			|||
		inner_key: priv_key,
 | 
				
			|||
	};
 | 
				
			|||
	Ok(key_pair)
 | 
				
			|||
}
 | 
				
			|||
@ -1,25 +0,0 @@ | 
				
			|||
use openssl::nid::Nid;
 | 
				
			|||
 | 
				
			|||
pub type SubjectAttribute = super::BaseSubjectAttribute;
 | 
				
			|||
 | 
				
			|||
impl SubjectAttribute {
 | 
				
			|||
	pub fn get_nid(&self) -> Nid {
 | 
				
			|||
		match self {
 | 
				
			|||
			SubjectAttribute::CountryName => Nid::COUNTRYNAME,
 | 
				
			|||
			SubjectAttribute::GenerationQualifier => Nid::GENERATIONQUALIFIER,
 | 
				
			|||
			SubjectAttribute::GivenName => Nid::GIVENNAME,
 | 
				
			|||
			SubjectAttribute::Initials => Nid::INITIALS,
 | 
				
			|||
			SubjectAttribute::LocalityName => Nid::LOCALITYNAME,
 | 
				
			|||
			SubjectAttribute::Name => Nid::NAME,
 | 
				
			|||
			SubjectAttribute::OrganizationName => Nid::ORGANIZATIONNAME,
 | 
				
			|||
			SubjectAttribute::OrganizationalUnitName => Nid::ORGANIZATIONALUNITNAME,
 | 
				
			|||
			SubjectAttribute::Pkcs9EmailAddress => Nid::PKCS9_EMAILADDRESS,
 | 
				
			|||
			SubjectAttribute::PostalAddress => Nid::POSTALADDRESS,
 | 
				
			|||
			SubjectAttribute::PostalCode => Nid::POSTALCODE,
 | 
				
			|||
			SubjectAttribute::StateOrProvinceName => Nid::STATEORPROVINCENAME,
 | 
				
			|||
			SubjectAttribute::Street => Nid::STREETADDRESS,
 | 
				
			|||
			SubjectAttribute::Surname => Nid::SURNAME,
 | 
				
			|||
			SubjectAttribute::Title => Nid::TITLE,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,27 +0,0 @@ | 
				
			|||
pub fn get_lib_name() -> String {
 | 
				
			|||
	env!("ACMED_TLS_LIB_NAME").to_string()
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn get_lib_version() -> String {
 | 
				
			|||
	let v = openssl::version::number() as u64;
 | 
				
			|||
	let mut version = vec![];
 | 
				
			|||
	for i in 0..3 {
 | 
				
			|||
		let n = get_openssl_version_unit(v, i);
 | 
				
			|||
		version.push(format!("{n}"));
 | 
				
			|||
	}
 | 
				
			|||
	let version = version.join(".");
 | 
				
			|||
	let p = get_openssl_version_unit(v, 3);
 | 
				
			|||
	if p != 0 {
 | 
				
			|||
		let p = p + 0x60;
 | 
				
			|||
		let p = std::char::from_u32(p as u32).unwrap();
 | 
				
			|||
		format!("{version}{p}")
 | 
				
			|||
	} else {
 | 
				
			|||
		version
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_openssl_version_unit(n: u64, pos: u32) -> u64 {
 | 
				
			|||
	let p = 0x000f_f000_0000 >> (8 * pos);
 | 
				
			|||
	let n = n & p;
 | 
				
			|||
	n >> (8 * (3 - pos) + 4)
 | 
				
			|||
}
 | 
				
			|||
@ -1,133 +0,0 @@ | 
				
			|||
use std::fmt;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct Error {
 | 
				
			|||
	pub message: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Error {
 | 
				
			|||
	pub fn prefix(&self, prefix: &str) -> Self {
 | 
				
			|||
		Error {
 | 
				
			|||
			message: format!("{prefix}: {}", &self.message),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Error {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		write!(f, "{}", self.message)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<&str> for Error {
 | 
				
			|||
	fn from(error: &str) -> Self {
 | 
				
			|||
		Error {
 | 
				
			|||
			message: error.to_string(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<String> for Error {
 | 
				
			|||
	fn from(error: String) -> Self {
 | 
				
			|||
		error.as_str().into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<&String> for Error {
 | 
				
			|||
	fn from(error: &String) -> Self {
 | 
				
			|||
		error.as_str().into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<std::io::Error> for Error {
 | 
				
			|||
	fn from(error: std::io::Error) -> Self {
 | 
				
			|||
		format!("IO error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<std::net::AddrParseError> for Error {
 | 
				
			|||
	fn from(error: std::net::AddrParseError) -> Self {
 | 
				
			|||
		format!("{error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<std::string::FromUtf8Error> for Error {
 | 
				
			|||
	fn from(error: std::string::FromUtf8Error) -> Self {
 | 
				
			|||
		format!("UTF-8 error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<std::sync::mpsc::RecvError> for Error {
 | 
				
			|||
	fn from(error: std::sync::mpsc::RecvError) -> Self {
 | 
				
			|||
		format!("MSPC receiver error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<std::time::SystemTimeError> for Error {
 | 
				
			|||
	fn from(error: std::time::SystemTimeError) -> Self {
 | 
				
			|||
		format!("SystemTimeError difference: {:?}", error.duration()).into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<base64::DecodeError> for Error {
 | 
				
			|||
	fn from(error: base64::DecodeError) -> Self {
 | 
				
			|||
		format!("base 64 decode error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<syslog::Error> for Error {
 | 
				
			|||
	fn from(error: syslog::Error) -> Self {
 | 
				
			|||
		format!("syslog error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<toml::de::Error> for Error {
 | 
				
			|||
	fn from(error: toml::de::Error) -> Self {
 | 
				
			|||
		format!("IO error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<serde_json::error::Error> for Error {
 | 
				
			|||
	fn from(error: serde_json::error::Error) -> Self {
 | 
				
			|||
		format!("IO error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<reqwest::Error> for Error {
 | 
				
			|||
	fn from(error: reqwest::Error) -> Self {
 | 
				
			|||
		format!("HTTP error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<glob::PatternError> for Error {
 | 
				
			|||
	fn from(error: glob::PatternError) -> Self {
 | 
				
			|||
		format!("pattern error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<minijinja::Error> for Error {
 | 
				
			|||
	fn from(error: minijinja::Error) -> Self {
 | 
				
			|||
		format!("template error: {error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
impl From<native_tls::Error> for Error {
 | 
				
			|||
	fn from(error: native_tls::Error) -> Self {
 | 
				
			|||
		format!("{error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
impl From<openssl::error::ErrorStack> for Error {
 | 
				
			|||
	fn from(error: openssl::error::ErrorStack) -> Self {
 | 
				
			|||
		format!("{error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(unix)]
 | 
				
			|||
impl From<nix::Error> for Error {
 | 
				
			|||
	fn from(error: nix::Error) -> Self {
 | 
				
			|||
		format!("{error}").into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,76 +0,0 @@ | 
				
			|||
use base64::Engine;
 | 
				
			|||
use daemonize::Daemonize;
 | 
				
			|||
use std::fs::File;
 | 
				
			|||
use std::io::prelude::*;
 | 
				
			|||
use std::{fs, process};
 | 
				
			|||
 | 
				
			|||
pub mod crypto;
 | 
				
			|||
pub mod error;
 | 
				
			|||
pub mod logs;
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests;
 | 
				
			|||
 | 
				
			|||
macro_rules! exit_match {
 | 
				
			|||
	($e: expr) => {
 | 
				
			|||
		match $e {
 | 
				
			|||
			Ok(_) => {}
 | 
				
			|||
			Err(e) => {
 | 
				
			|||
				log::error!("error: {e}");
 | 
				
			|||
				std::process::exit(3);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn to_idna(domain_name: &str) -> Result<String, error::Error> {
 | 
				
			|||
	let mut idna_parts = vec![];
 | 
				
			|||
	let parts: Vec<&str> = domain_name.split('.').collect();
 | 
				
			|||
	for name in parts.iter() {
 | 
				
			|||
		let raw_name = name.to_lowercase();
 | 
				
			|||
		let idna_name = if name.is_ascii() {
 | 
				
			|||
			raw_name
 | 
				
			|||
		} else {
 | 
				
			|||
			let idna_name = punycode::encode(&raw_name)
 | 
				
			|||
				.map_err(|_| error::Error::from("IDNA encoding failed."))?;
 | 
				
			|||
			format!("xn--{idna_name}")
 | 
				
			|||
		};
 | 
				
			|||
		idna_parts.push(idna_name);
 | 
				
			|||
	}
 | 
				
			|||
	Ok(idna_parts.join("."))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn b64_encode<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
 | 
				
			|||
	base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn b64_decode<T: ?Sized + AsRef<[u8]>>(input: &T) -> Result<Vec<u8>, error::Error> {
 | 
				
			|||
	let res = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)?;
 | 
				
			|||
	Ok(res)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn init_server(foreground: bool, pid_file: Option<&str>) {
 | 
				
			|||
	if !foreground {
 | 
				
			|||
		let mut daemonize = Daemonize::new();
 | 
				
			|||
		if let Some(f) = pid_file {
 | 
				
			|||
			daemonize = daemonize.pid_file(f);
 | 
				
			|||
		}
 | 
				
			|||
		exit_match!(daemonize.start());
 | 
				
			|||
	} else if let Some(f) = pid_file {
 | 
				
			|||
		exit_match!(write_pid_file(f).map_err(|e| e.prefix(f)));
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn write_pid_file(pid_file: &str) -> Result<(), error::Error> {
 | 
				
			|||
	let data = format!("{}\n", process::id()).into_bytes();
 | 
				
			|||
	let mut file = File::create(pid_file)?;
 | 
				
			|||
	file.write_all(&data)?;
 | 
				
			|||
	file.sync_all()?;
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn clean_pid_file(pid_file: Option<&str>) -> Result<(), error::Error> {
 | 
				
			|||
	if let Some(f) = pid_file {
 | 
				
			|||
		fs::remove_file(f)?;
 | 
				
			|||
	}
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
@ -1,86 +0,0 @@ | 
				
			|||
use crate::error::Error;
 | 
				
			|||
use env_logger::Builder;
 | 
				
			|||
use log::LevelFilter;
 | 
				
			|||
use syslog::Facility;
 | 
				
			|||
 | 
				
			|||
pub const DEFAULT_LOG_SYSTEM: LogSystem = LogSystem::SysLog;
 | 
				
			|||
pub const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Warn;
 | 
				
			|||
 | 
				
			|||
#[derive(Debug, PartialEq, Eq)]
 | 
				
			|||
pub enum LogSystem {
 | 
				
			|||
	SysLog,
 | 
				
			|||
	StdErr,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_loglevel(log_level: Option<&str>) -> Result<LevelFilter, Error> {
 | 
				
			|||
	let level = match log_level {
 | 
				
			|||
		Some(v) => match v {
 | 
				
			|||
			"error" => LevelFilter::Error,
 | 
				
			|||
			"warn" => LevelFilter::Warn,
 | 
				
			|||
			"info" => LevelFilter::Info,
 | 
				
			|||
			"debug" => LevelFilter::Debug,
 | 
				
			|||
			"trace" => LevelFilter::Trace,
 | 
				
			|||
			_ => {
 | 
				
			|||
				return Err(format!("{v}: invalid log level").into());
 | 
				
			|||
			}
 | 
				
			|||
		},
 | 
				
			|||
		None => DEFAULT_LOG_LEVEL,
 | 
				
			|||
	};
 | 
				
			|||
	Ok(level)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn set_log_syslog(log_level: LevelFilter) -> Result<(), Error> {
 | 
				
			|||
	syslog::init(
 | 
				
			|||
		Facility::LOG_DAEMON,
 | 
				
			|||
		log_level,
 | 
				
			|||
		Some(env!("CARGO_PKG_NAME")),
 | 
				
			|||
	)?;
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn set_log_stderr(log_level: LevelFilter) -> Result<(), Error> {
 | 
				
			|||
	let mut builder = Builder::from_env("ACMED_LOG_LEVEL");
 | 
				
			|||
	builder.filter_level(log_level);
 | 
				
			|||
	builder.init();
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn set_log_system(
 | 
				
			|||
	log_level: Option<&str>,
 | 
				
			|||
	has_syslog: bool,
 | 
				
			|||
	has_stderr: bool,
 | 
				
			|||
) -> Result<(LogSystem, LevelFilter), Error> {
 | 
				
			|||
	let log_level = get_loglevel(log_level)?;
 | 
				
			|||
	let logtype = if has_syslog {
 | 
				
			|||
		LogSystem::SysLog
 | 
				
			|||
	} else if has_stderr {
 | 
				
			|||
		LogSystem::StdErr
 | 
				
			|||
	} else {
 | 
				
			|||
		DEFAULT_LOG_SYSTEM
 | 
				
			|||
	};
 | 
				
			|||
	match logtype {
 | 
				
			|||
		LogSystem::SysLog => set_log_syslog(log_level)?,
 | 
				
			|||
		LogSystem::StdErr => set_log_stderr(log_level)?,
 | 
				
			|||
	};
 | 
				
			|||
	Ok((logtype, log_level))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::{set_log_system, DEFAULT_LOG_LEVEL, DEFAULT_LOG_SYSTEM};
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_invalid_level() {
 | 
				
			|||
		let ret = set_log_system(Some("invalid"), false, false);
 | 
				
			|||
		assert!(ret.is_err());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_default_values() {
 | 
				
			|||
		let ret = set_log_system(None, false, false);
 | 
				
			|||
		assert!(ret.is_ok());
 | 
				
			|||
		let (logtype, log_level) = ret.unwrap();
 | 
				
			|||
		assert_eq!(logtype, DEFAULT_LOG_SYSTEM);
 | 
				
			|||
		assert_eq!(log_level, DEFAULT_LOG_LEVEL);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,5 +0,0 @@ | 
				
			|||
mod certificate;
 | 
				
			|||
mod crypto_keys;
 | 
				
			|||
mod hash;
 | 
				
			|||
mod idna;
 | 
				
			|||
mod jws_signature_algorithm;
 | 
				
			|||
@ -1,181 +0,0 @@ | 
				
			|||
use crate::crypto::{HashFunction, KeyType, X509Certificate, CRT_NB_DAYS_VALIDITY};
 | 
				
			|||
use std::collections::HashSet;
 | 
				
			|||
use std::iter::FromIterator;
 | 
				
			|||
 | 
				
			|||
const CERTIFICATE_P256_DOMAINS_PEM: &str = r#"-----BEGIN CERTIFICATE-----
 | 
				
			|||
MIICtDCCAZygAwIBAgIIf5BEPlNrrYkwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
 | 
				
			|||
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAyZDE2ODgwHhcNMjAwODI1MTMwMzE3
 | 
				
			|||
WhcNMjUwODI1MTMwMzE3WjAYMRYwFAYDVQQDEw1sb2NhbC53aGF0LnRmMFkwEwYH
 | 
				
			|||
KoZIzj0CAQYIKoZIzj0DAQcDQgAE0c/unUqpoOMxxc8e1pkpPQTSsh2irQruOJgd
 | 
				
			|||
ITN9WLC4mzFSJ/ad64TFi4HsCFNd7mv/QRH6rW1s3LbocEvBuqOBvDCBuTAOBgNV
 | 
				
			|||
HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
 | 
				
			|||
EwEB/wQCMAAwHQYDVR0OBBYEFGjf1TWIZyE+QP9SGkBN6dfviIsaMB8GA1UdIwQY
 | 
				
			|||
MBaAFLgD5DMU2ijpIxlxaAv82sQvb5ofMDoGA1UdEQQzMDGCDWxvY2FsLndoYXQu
 | 
				
			|||
dGaCDzEubG9jYWwud2hhdC50ZoIPMi5sb2NhbC53aGF0LnRmMA0GCSqGSIb3DQEB
 | 
				
			|||
CwUAA4IBAQDREOAU2JwHfSPGt4SYlQ3OmFl4HHI2f+XyNE/09uZVteM0aChkntgX
 | 
				
			|||
rAZltuAAX+coSlgv3a04hJBqioDG1R9MFtf4LZBhfkgZwbzucMt8Ga3QL3XFXOkn
 | 
				
			|||
FlOwb/ZEIjFsBFQWt1ZSA85WxIVkGsgMfQeGpu/p8gEmJAE5l0qHEVFP9cYNsIqg
 | 
				
			|||
wsUGwZzPZFLsBXurM2cEA7cTt2HryVXlQWl8QI5YFpIpa43itYaerfMldfIfNdJ9
 | 
				
			|||
8GLZPEfJb6t/UYYexXEkpQY9wGZkaTWvYeItuC0YlPY9RUCAl48Q85Yjf37Wbm5z
 | 
				
			|||
f810HGl+/c6ttyoHKmLfY/GcX07AUcLc
 | 
				
			|||
-----END CERTIFICATE-----"#;
 | 
				
			|||
const CERTIFICATE_P256_IP_PEM: &str = r#"-----BEGIN CERTIFICATE-----
 | 
				
			|||
MIICkTCCAXmgAwIBAgIIMW1X7DjQOFgwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
 | 
				
			|||
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxYWM3MzcwHhcNMjAwODI1MTQzMjQw
 | 
				
			|||
WhcNMjUwODI1MTQzMjQwWjAOMQwwCgYDVQQDEwM6OjEwWTATBgcqhkjOPQIBBggq
 | 
				
			|||
hkjOPQMBBwNCAASF+MvxX7GBAVe3McuAc+0emdFpBfAQG4mt9j8417qT76qHHyJ6
 | 
				
			|||
oIHRNXAUxh4J78ihrvyph8TvqND73Nxk8Jj9o4GjMIGgMA4GA1UdDwEB/wQEAwIF
 | 
				
			|||
oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd
 | 
				
			|||
BgNVHQ4EFgQU5R7EGzjpZqrs2o/ZwuBqNHlMB2AwHwYDVR0jBBgwFoAUhEUnWREW
 | 
				
			|||
GoAScr1wv/aXHTGOVoswIQYDVR0RBBowGIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAA
 | 
				
			|||
ATANBgkqhkiG9w0BAQsFAAOCAQEAS8oRpjGakUU+KRtXCGoVlXgKYFe3u/G2aFMF
 | 
				
			|||
soApjvwd3L1W9b3bsT4FquF7F5qB6TGBwiXoNBoDAeVhRcUsHbmN8GZRUaq2TEsm
 | 
				
			|||
MwpPr8L4rqeRIuxY85AqmbGfMuFUie6r4FbwelnBniO0eMQkTW/XY41rbhGZ+lmj
 | 
				
			|||
DTQy08oj0892py2U/YbkL3JnCBwBba//f/Ji7nnSKdJl4Yd1iguA0nbdElcWaKk3
 | 
				
			|||
ij3t17FSNeI5uMOI3TRBr4k4bu3ZMnuN2DYFPnL6GiSEhyNrxaiac8xKuOXBICmJ
 | 
				
			|||
oyO7pZVvc5cDcP/USPcWJYcnR9gvuL8snQdFpWND8H19eZ+i0g==
 | 
				
			|||
-----END CERTIFICATE-----"#;
 | 
				
			|||
const CERTIFICATE_P256_DOMAINS_IP_PEM: &str = r#"-----BEGIN CERTIFICATE-----
 | 
				
			|||
MIICzDCCAbSgAwIBAgIIff0SyxJBhtMwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
 | 
				
			|||
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxYWM3MzcwHhcNMjAwODI1MTQzNjE1
 | 
				
			|||
WhcNMjUwODI1MTQzNjE1WjAYMRYwFAYDVQQDEw1sb2NhbC53aGF0LnRmMFkwEwYH
 | 
				
			|||
KoZIzj0CAQYIKoZIzj0DAQcDQgAE7Jp4AmF0TTcYfUy4TtZhN4bXn4DXWnqF0I6i
 | 
				
			|||
Yvz4kc0r2L01nrUrICg2bmCFM7BU9pr9fcCDodH3ZuhlRqBAf6OB1DCB0TAOBgNV
 | 
				
			|||
HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
 | 
				
			|||
EwEB/wQCMAAwHQYDVR0OBBYEFHV0lnh55aQGfljcsjNkzZa4lTG6MB8GA1UdIwQY
 | 
				
			|||
MBaAFIRFJ1kRFhqAEnK9cL/2lx0xjlaLMFIGA1UdEQRLMEmCDWxvY2FsLndoYXQu
 | 
				
			|||
dGaCDzEubG9jYWwud2hhdC50ZoIPMi5sb2NhbC53aGF0LnRmhwR/AAABhxAAAAAA
 | 
				
			|||
AAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQC3VmoTlrrTCWCd4eUB4RSB
 | 
				
			|||
+080uco6Jl7VMqcY5F+eG1S7p4Kqz6kc1wiiKB8ILA94hdP1qTbfphdGllYiEvbs
 | 
				
			|||
urj0x62cm5URahEDx4xn+dQkmh4XiiZgZVw2ccphjqJqJa28GsuR2zAxSkKMDnB7
 | 
				
			|||
eX1G4/Av0XE7RqJ3Frq8qa5EjjLJTw0iEaWS5NGtZxMqWEIetCgb0IDZNxNvbeAv
 | 
				
			|||
mmH6qnF3xQPx5FkwP/Yw4d9T4KhSHNf2/tImIlbuk3SEsOglGbKNY1juor8uw+J2
 | 
				
			|||
5XsUZxD5QiDbCFd3dGmH58XmkiQHXs8hhIbhu9ZLgp+fNv0enVMHTTI1gGpZ5MPm
 | 
				
			|||
-----END CERTIFICATE-----"#;
 | 
				
			|||
const CERTIFICATE_EXPIRED_PEM: &str = r#"-----BEGIN CERTIFICATE-----
 | 
				
			|||
MIIEsTCCA5mgAwIBAgISBApMImYflPdX7BYLjinQ+ErUMA0GCSqGSIb3DQEBCwUA
 | 
				
			|||
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
 | 
				
			|||
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTExMzAxODQxNTZaFw0y
 | 
				
			|||
MDAyMjgxODQxNTZaMBExDzANBgNVBAMTBmJ6aC50ZjB2MBAGByqGSM49AgEGBSuB
 | 
				
			|||
BAAiA2IABLSEIYJpT2SM+F9mEzFypkqbBm64dgX0KnyZuYGB2qHHsBLIBBK5Ev9Y
 | 
				
			|||
vPvYb8lzX3uJFHPn0JwPpGR0YBzPHBspyvwrhedokt8pNFEDC1eE4BH9XVN35utt
 | 
				
			|||
EGP1ZT92mKOCAnYwggJyMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEF
 | 
				
			|||
BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUOALpvHYbvHbQ
 | 
				
			|||
GcrtL0I4s/W/S58wHwYDVR0jBBgwFoAUqEpqYwR93brm0Tm3pkVl7/Oo7KEwbwYI
 | 
				
			|||
KwYBBQUHAQEEYzBhMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcC5pbnQteDMubGV0
 | 
				
			|||
c2VuY3J5cHQub3JnMC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDMubGV0
 | 
				
			|||
c2VuY3J5cHQub3JnLzAtBgNVHREEJjAkggZiemgudGaCDm10YS1zdHMuYnpoLnRm
 | 
				
			|||
ggp3d3cuYnpoLnRmMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEB
 | 
				
			|||
MCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBAwYK
 | 
				
			|||
KwYBBAHWeQIEAgSB9ASB8QDvAHYAb1N2rDHwMRnYmQCkURX/dxUcEdkCwQApBo2y
 | 
				
			|||
CJo32RMAAAFuvdWC7QAABAMARzBFAiBgCoazSI4unyx09P8KYxdIfMZsG/fMtzkF
 | 
				
			|||
ciBDB9gcJQIhAPZMsnjqr4IqpyHyvauqrWoGqlFBcBCmogZCuhQXAnv5AHUAB7dc
 | 
				
			|||
G+V9aP/xsMYdIxXHuuZXfFeUt2ruvGE6GmnTohwAAAFuvdWC7gAABAMARjBEAiAO
 | 
				
			|||
z7sHUA42VEQkicrWb5A4WjNGWV7NxpSDdb2XQ2Q1OwIgRaiEMrHfyT797O7Fvbk2
 | 
				
			|||
cL6rnnmDJOyxIAC4Dxe7NVwwDQYJKoZIhvcNAQELBQADggEBAFaNvfsGKqBuJ9m7
 | 
				
			|||
qRNqVmC7UHzGym+TPBLiXncwFIaWt0ncRHb6qfGCCETeAplhPv8uoOrzQQwTKwr3
 | 
				
			|||
eMDtdmK+9smnQZ4AjUsscsrbkGwMWOOmIRm/tCwQZ0dFnl1ySZDuaoCG7v/uRE4A
 | 
				
			|||
HXtNAeVOKuE7BOISvvssFajxLifmFixifWRwEnimTffjnIX6xqol+2bcxMuLWxt9
 | 
				
			|||
HmjTgcY4JMMcOAiNk3roJK9ayMi7jn0Cd097BFnvx08+oWSMOZ29hFHMHp3KCSzT
 | 
				
			|||
bQg4DAU6E9VT+pvyGsc1NNyREKxOlDkam3CqfYc0oAowjn11MmDac2aKP8Pyt4pk
 | 
				
			|||
ehm+yKg=
 | 
				
			|||
-----END CERTIFICATE-----"#;
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_san_domains() {
 | 
				
			|||
	let san = vec!["local.what.tf", "1.local.what.tf", "2.local.what.tf"];
 | 
				
			|||
	let san = HashSet::from_iter(san.iter().map(|v| v.to_string()));
 | 
				
			|||
	let crt = X509Certificate::from_pem(CERTIFICATE_P256_DOMAINS_PEM.as_bytes()).unwrap();
 | 
				
			|||
	assert_eq!(crt.subject_alt_names(), san);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_san_ip() {
 | 
				
			|||
	let san = vec!["127.0.0.1", "::1"];
 | 
				
			|||
	let san = HashSet::from_iter(san.iter().map(|v| v.to_string()));
 | 
				
			|||
	let crt = X509Certificate::from_pem(CERTIFICATE_P256_IP_PEM.as_bytes()).unwrap();
 | 
				
			|||
	assert_eq!(crt.subject_alt_names(), san);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_san_domains_and_ip() {
 | 
				
			|||
	let san = vec![
 | 
				
			|||
		"127.0.0.1",
 | 
				
			|||
		"::1",
 | 
				
			|||
		"local.what.tf",
 | 
				
			|||
		"1.local.what.tf",
 | 
				
			|||
		"2.local.what.tf",
 | 
				
			|||
	];
 | 
				
			|||
	let san = HashSet::from_iter(san.iter().map(|v| v.to_string()));
 | 
				
			|||
	let crt = X509Certificate::from_pem(CERTIFICATE_P256_DOMAINS_IP_PEM.as_bytes()).unwrap();
 | 
				
			|||
	assert_eq!(crt.subject_alt_names(), san);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn generate_rsa2048_certificate() {
 | 
				
			|||
	let (kp, _) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::Rsa2048, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	assert_eq!(kp.key_type, KeyType::Rsa2048);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn generate_rsa4096_certificate() {
 | 
				
			|||
	let (kp, _) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::Rsa4096, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	assert_eq!(kp.key_type, KeyType::Rsa4096);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn generate_ecdsa_p256_certificate() {
 | 
				
			|||
	let (kp, _) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::EcdsaP256, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	assert_eq!(kp.key_type, KeyType::EcdsaP256);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn generate_ecdsa_p384_certificate() {
 | 
				
			|||
	let (kp, _) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::EcdsaP384, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	assert_eq!(kp.key_type, KeyType::EcdsaP384);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn generate_ed25519_certificate() {
 | 
				
			|||
	let (kp, _) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::Ed25519, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	assert_eq!(kp.key_type, KeyType::Ed25519);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed448")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn generate_ed448_certificate() {
 | 
				
			|||
	let (kp, _) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::Ed448, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	assert_eq!(kp.key_type, KeyType::Ed448);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn cert_expiration_date_future() {
 | 
				
			|||
	let (_, crt) =
 | 
				
			|||
		X509Certificate::from_acme_ext("example.org", "", KeyType::EcdsaP256, HashFunction::Sha256)
 | 
				
			|||
			.unwrap();
 | 
				
			|||
	let duration = crt.expires_in().unwrap().as_secs();
 | 
				
			|||
	let validity_sec = CRT_NB_DAYS_VALIDITY as u64 * 24 * 60 * 60;
 | 
				
			|||
	let delta = 60;
 | 
				
			|||
	assert!(duration > validity_sec - delta);
 | 
				
			|||
	assert!(duration < validity_sec + delta);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn cert_expiration_date_past() {
 | 
				
			|||
	let crt = X509Certificate::from_pem(CERTIFICATE_EXPIRED_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let duration = crt.expires_in().unwrap().as_secs();
 | 
				
			|||
	assert_eq!(duration, 0);
 | 
				
			|||
}
 | 
				
			|||
@ -1,411 +0,0 @@ | 
				
			|||
use crate::crypto::KeyPair;
 | 
				
			|||
 | 
				
			|||
const KEY_RSA_2048_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzfwZGF8zKNAg2
 | 
				
			|||
9mdZ9ieE7V2clY3oeI+2V7eV5kUwOGqhhpDaDyDmju+l0dKFwF8xeDeeGmTSED10
 | 
				
			|||
e38ZsHqJF0cZqKDrB3hOeDAsn7Z6stHf/RZozQO5sAmZpN7g0P0lhXJnyAr+WL58
 | 
				
			|||
X41kWuufiPVbvURQv/tK3yN2K+rC6MdZ2lLsLemKiwAlbyGrPfUzuVc6dXrU8JvX
 | 
				
			|||
kkwuIpAyEEJ7OTXdBaT4VAHHtm2YDWIwW+34Otyp2FvbSJYsIwJjC00t7Phmah9b
 | 
				
			|||
MjiypCZB6OknZV7WAZ55jaF/rypARB/zzTieSyn4Qi/VjipWE7nO/GjubyzrJtQm
 | 
				
			|||
q+o7Pm71AgMBAAECggEAVAXEFA+UB5svtTrGym/Vs/3A8kl3sjitXTfWck7mWFow
 | 
				
			|||
YAgzyj+GsSZ7u+1qVL3mUavqrRHB3CtJ+TrOFmJsGbxRxgsPuLU4ddMBCgKBUxJd
 | 
				
			|||
+DHqyYgelE95TvjEdAygU24STc5whvtXv7Si5TVCUt2zrQv97KbRpQyq9ug77pxp
 | 
				
			|||
iQGiZ4spUH47TrYtw85HqU1Vb+hJamvcwLv1jv6sKOKv4A4nF3OsqJOqH1FAcFf7
 | 
				
			|||
f2Co2Zz83LV6WZ+yFAVG4C1OFMYJABHb3Sq+a5BOipkcCqQqK016NBcIsPvMGTuK
 | 
				
			|||
sHUBa2Reh9jLdOehfUa3p+Ir9ZALD+gs5jStRxFqCQKBgQD6Vcpsspsrl069fIJ2
 | 
				
			|||
gWd37saM0b0DTqf5Pb3JFKyD5yCyRQD0UtgrUSP8wPxhRtJN0Jku+X1IW3FjLPeg
 | 
				
			|||
S/VWEp2nmRTpvHGZ1KYD0gn3RQne8mbt43+f9AwlEfjhvrWDUQhb1TOdCwa/9/xY
 | 
				
			|||
HPRM0xV4UiYJG+GVLla4Rbs60wKBgQC3jtvZh/Nd8DwtuS8wXMHQxTLjiHdd6r5n
 | 
				
			|||
Lm1m6236NHs7NMA3NlcH9lOP+YfU3I0Ti4CnYI8YWyIrJAbck6maCzLlzUluSzeo
 | 
				
			|||
kJ+Ax0/H7DOM0ix7EMkUMCU8m5qi684qg1yngmWobd0Y3aCWjPgQa0oG04+uXb0A
 | 
				
			|||
w+GbrB+CFwKBgQDGfF1a4CauYnMZRO7AfYwHiPg+0VH3nFcNBQpEtDKxBwJittmx
 | 
				
			|||
3zns5pINJws1Kg03i6zZlRHj3DVEOHRC0dc9ntcH+xWc2kCMgxH6t4AVYdUYw8Qe
 | 
				
			|||
3KHltoAmqGBYxXhwHUDuZ1ZcL1DzxvF6/8IoY7mDREdKM6QiP7KcuxVf5wKBgHx/
 | 
				
			|||
NnnqDZRvNkHE0k64+vPAbG2Kx3s5lf6hrK4bjDIhmltjweMwxgKufaqvEgO7uyvA
 | 
				
			|||
eHgNs8BPP3OHMeg1dtj2M4VNoTpfZda8kJJlnKT6fVRL0MN/dQJuTTM4Tr+ls+V9
 | 
				
			|||
x0AN3ylHqqgM2biC0FVCj6jloRQgm+qC8OgG7C/tAoGAeMToPctEvifliZkiyA6P
 | 
				
			|||
INrBwWyg3d0Kk3Wlyne3HP9PwS5KrtbKqwkAXsFWNW0HMpG7lMkedvoYjmL71i/5
 | 
				
			|||
jIkjfccxlH1fRp/YOZ0wJ4ZWS6G/QgfqmvTeIpEcbokOmBGvHeuFLA8pyzfZa9rP
 | 
				
			|||
hEeZwTjgMoKYaVZ4q+23m/0=
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
const KEY_RSA_4096_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCObQmHxmT5pGpT
 | 
				
			|||
EwTTIt4eEG10MWroi9V6yv998XAaq2Uji0hroypi644TmqWqgGvG3V+lxyoYXzaP
 | 
				
			|||
jsmp6hZ6uQ11nFiHW4oZBr0ba8Hg4E4UfFi6NP4008+XCdU+unX+mPErSGI4bNlG
 | 
				
			|||
WI2E7rmUxsS55AJottDrCy2vhpWxYqLDw7gwbwJzZl6CRaViDRkc4Cf64mUC3Xs0
 | 
				
			|||
l9fzHnvpdHBfYoKyw9h7KLAG6RGd7dqFVWqtzfkSZX0kfcLENob0x4EujsVRXk/U
 | 
				
			|||
Y04izfXdxsCB0xhXoCGvYu+xou5wW+FLCYeHjb76Z4O6L7TB0UguOvPsywhNnLjF
 | 
				
			|||
eOXz/OoxnZ0Fo7fzA/mj1UJ8JdpWUPchgH0Dj5j0hDrARV6h3MaWg9DJ11RnvSRn
 | 
				
			|||
s7cTWRi7S0EqGgAvVVwqJS86PUAEaiip/xAgBYKIBaoOqRknkeJHrU0L2Uncwf5q
 | 
				
			|||
phfN4dS47F2V+vehY9AqVHJihRb3fEPh1eMdDIewhLiBZgT1kn1oiiY0eP3dQ819
 | 
				
			|||
HxJzFBNAElX1O2CX+85R2aVqxOajQ5dtMsx7HNYJUhK9S6gGhNxNjgNcIV5gfPcQ
 | 
				
			|||
TeKMn1+cORfdKy9XjfHxfnynL2sTIBCdHF95qGlYSjFbpMDmoHGDUYwFC7oJjYy6
 | 
				
			|||
mqyYjRcdNfPLzEK3g+p39rnM/paH8QIDAQABAoICAFT0jV7D5K9Ud2eeTJ50ifF8
 | 
				
			|||
8wz//TlBT9GzDLtfLPN7kRSmnEg4R6xBvbnL4U3W1HMG0WrdZiqrgKwZDAmibE4/
 | 
				
			|||
29tvqw7yd2l+L4cPu9IbefeWRIat3YQ9Y/JAF0cXihKXwCOFRbFKnD/tylyk2WX5
 | 
				
			|||
Opd3fkhf5DaPsGym5tusblI/iLq7PMcBJRan3IKkNXqX6sEoEgCnhDpW6KVIZblX
 | 
				
			|||
j0AWTse7MoIkPvugQrXljxdBYCTUW+GxT/hYW7kWnWGdL11KJEDo9M1HfvAb0rC7
 | 
				
			|||
QVEvTbHW/sDTTw6ylW/IHpbX1FPzJRvQay7ADh4ea+PHnoB8izNgbIa+Gsxy7G5M
 | 
				
			|||
sc2aCrQu5ywRBmYLkTzxHu08Xfl7ZB1R7hczqznMd769MnlpIiQd6QbsbTrh2s5N
 | 
				
			|||
Yq4EtxOOFjU66XNBtYjn7h1yN2nWzjwVONgxcDQwacdkYD//IUryma6rF0UEXEDD
 | 
				
			|||
gBrdS4Q/28f0HmbWOh+qpqERb8YVLWL+VQy1OI4/9VIDDJDM76KxZKkJ/uE0FD9Z
 | 
				
			|||
Fj97ZjUfxg9D14ynJ6rsp0cEx8Q+h8tep6yEj1hdO6+72JhvR2IrJMOPDooV5hnY
 | 
				
			|||
7fZMOceKGKE+N1afZXqZXW7vRlSnpmE+HMgYHVQyPWbZ1I1KC9RhtI5fxiyc8V5j
 | 
				
			|||
c9dqdstEruvZ9cAPrfABAoIBAQDju4k7gtd6AFOnybYFQ027KJWD3QGharlWN9dL
 | 
				
			|||
r4D0yHj/btCdiwSf1WbQm9uFTjArRgahraQ7WbtvHBRM1JQ+BNAoY7Djaf/fHkYX
 | 
				
			|||
OAXSXE/56I6YwxTd/iVFiYs+G9wD90waC5/dMjcp7kTA63oIIVCgOln20wYwCZUO
 | 
				
			|||
4pf6qSi9tLrEvg5EWeZHCDay7As8xZXELsa9ao2Bt/zhNDrpSPR/AY7MYcbwqh5o
 | 
				
			|||
iWI43FADSL2k6dU12IRPTOyAxiV+oYYeJcI7BJrADVay7zZmIMilyKfWNp3yHUc+
 | 
				
			|||
AdSOSSDmoz2cKej8ScoHaiOtnvwy3wG11eWzSeoqRy63DA+xAoIBAQCgGsjld9Y+
 | 
				
			|||
YWQi+k/6CUPSbAolDGo6eZ1YAVS6fJ2e7P09Ou0txCqPWjxJeVBMkEJoTERmjikZ
 | 
				
			|||
lC8NDLCr3PmHA/1AY7D40AhMVrroUa3wDI6KnB/LMT3l6L1sCt4N/EEalTAKomOT
 | 
				
			|||
jpMp2IWtHMGbYr7x6hg2CIoWSZUfpVDipMxAcT2ak18xRyWxJDnl2HgX4NbMKKwI
 | 
				
			|||
zQXy0vF5NrP5eg+9d2hvfpf+opGNdtVANkkXGpFxKqO6HuVYccijxY0NcrQ8r1gp
 | 
				
			|||
CnIFIVqNpAFoBqtwFaeHrkg1/GlYOajMLnRW+qZIV6K+n9n+SYbLu1KRHrl9xkn/
 | 
				
			|||
0MZSInMTPkxBAoIBAQDAMQwfIkxRlScEqrIn/OYD9rtAHut6W8RwZA4ZvNL7QpkD
 | 
				
			|||
EXWUD7fmYEY19eMsvJDgZGfCWPYKdK8/lRX4xUsakBtQitnFAzdDCJykic43+1ov
 | 
				
			|||
kbmOaM0akJrJ9cuCriZfXnxmWrsfBXsSsxhpLBG//MW7g6NbMDq/ncajWk5i6BIP
 | 
				
			|||
EBCza6ZEvw4dkmv/UkAlmKbNe6CUSPGFsU4EjXzOVpio+xqVmEs53ohtNsyjKiOI
 | 
				
			|||
sgICxKkAmWsINeY+w3rvRMgYd0tVXYxwWpF5z3I8fJx5dT9YBJ4Fr/no9ch6EHNo
 | 
				
			|||
0glz2tba3DdZTJUxuMQk9pnN6OfDCLVL2uks6EvxAoIBAQCCb0/cIpVYnN+H34Xo
 | 
				
			|||
nkOy2nIpXMPuf8XAPNVaWMvQ/iISED/KWVaTE2CqOztAJQb1Ea1oH8k8HY13hC8q
 | 
				
			|||
1Qw1Avr/yjgTfOhFySLcwi6CsrguFKOSVrum4sXvj6r4mdowXfqVr1aQkEc0gEHn
 | 
				
			|||
ltXkUb5eN+khnDNjlO74qSYMf1Yn6hnWJNoYu23psym4J3MvgO19xmThhqah/Vjc
 | 
				
			|||
98QIK3lHUlCzBN+vg6IxLe7uMUu6ltqG58Ybi7AtLgXX5snTeu97wR6B0RCzPUkY
 | 
				
			|||
u9Spe0WQOxQRZdtOoCTyy4bJUc9WTT3LEhp0Uqa2lBBNSn8p224jGbiPwPbRU1+M
 | 
				
			|||
/eQBAoIBAGTYVbha9dMdxlFnP63Cf2Ec7nraVSzm+6x414pCSosFTrl9eKI5dTV9
 | 
				
			|||
zUsLfYVgqWcqGN3S5Q/8lM6ppmZapaUFrgKHtKdYEUnWBeobnrKR4iUSyqxlAKtJ
 | 
				
			|||
fYfcw5ZfX8GHABopmKUC9UzarqhmM3Am423EGd1CUzseaWme52EUiAbbxSjlzhwM
 | 
				
			|||
Q2ZTyps7X64dx6yOIRv6pPd3qZGRz2VoKW2x/sLoeErPsVtUW0u+NSKgR6O5sh7v
 | 
				
			|||
Mc5vg/2W9HWaAXdjyrXIJyypitp0Q9M1cSowzt/BaWNvb3i/En8uEXR5zZjl/CFG
 | 
				
			|||
yr9E4nQyE5YlYlPUK6iIRBu9j1N2MhY=
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
const KEY_ECDSA_P256_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCQc9OXwvygYqOFT4fN
 | 
				
			|||
NpXynr1lu+1sSplFdYoWu7hE4g==
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
const KEY_ECDSA_P384_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDCMsN9kHPueLABk+0PKi7WO
 | 
				
			|||
PO2/53dpt/yV5zOPrYPEoKs4t973nbt46IUN19lLF/s=
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
const KEY_ECDSA_ED25519_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
MC4CAQAwBQYDK2VwBCIEIJhpRNsiUzoWqNkpJKCtKV5++Tttz3locu1gQKkQnrOa
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
const KEY_ECDSA_ED25519_PEM_BIS: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
MC4CAQAwBQYDK2VwBCIEIKa3WD0qeUToPQKSwa9cTsLPgCovqAtXMhlMX2KYBz0o
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
#[cfg(feature = "ed448")]
 | 
				
			|||
const KEY_ECDSA_ED448_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
 | 
				
			|||
MEcCAQAwBQYDK2VxBDsEOcFBwsH4zU7u5RgFh48MgJPzXyjN5uXxDapZv4rG6opU
 | 
				
			|||
uMXco2JR1CSjKWgqgu1CAKadJIYiv2EgIw==
 | 
				
			|||
-----END PRIVATE KEY-----"#;
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rsa_2048_jwk() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_RSA_2048_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 5);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("e"));
 | 
				
			|||
	assert!(jwk.contains_key("n"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "RSA");
 | 
				
			|||
	assert_eq!(jwk.get("e").unwrap(), "AQAB");
 | 
				
			|||
	assert_eq!(jwk.get("n").unwrap(), "s38GRhfMyjQINvZnWfYnhO1dnJWN6HiPtle3leZFMDhqoYaQ2g8g5o7vpdHShcBfMXg3nhpk0hA9dHt_GbB6iRdHGaig6wd4TngwLJ-2erLR3_0WaM0DubAJmaTe4ND9JYVyZ8gK_li-fF-NZFrrn4j1W71EUL_7St8jdivqwujHWdpS7C3piosAJW8hqz31M7lXOnV61PCb15JMLiKQMhBCezk13QWk-FQBx7ZtmA1iMFvt-Drcqdhb20iWLCMCYwtNLez4ZmofWzI4sqQmQejpJ2Ve1gGeeY2hf68qQEQf8804nksp-EIv1Y4qVhO5zvxo7m8s6ybUJqvqOz5u9Q");
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "RS256");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rsa_2048_jwk_thumbprint() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_RSA_2048_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 3);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("e"));
 | 
				
			|||
	assert!(jwk.contains_key("n"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "RSA");
 | 
				
			|||
	assert_eq!(jwk.get("e").unwrap(), "AQAB");
 | 
				
			|||
	assert_eq!(jwk.get("n").unwrap(), "s38GRhfMyjQINvZnWfYnhO1dnJWN6HiPtle3leZFMDhqoYaQ2g8g5o7vpdHShcBfMXg3nhpk0hA9dHt_GbB6iRdHGaig6wd4TngwLJ-2erLR3_0WaM0DubAJmaTe4ND9JYVyZ8gK_li-fF-NZFrrn4j1W71EUL_7St8jdivqwujHWdpS7C3piosAJW8hqz31M7lXOnV61PCb15JMLiKQMhBCezk13QWk-FQBx7ZtmA1iMFvt-Drcqdhb20iWLCMCYwtNLez4ZmofWzI4sqQmQejpJ2Ve1gGeeY2hf68qQEQf8804nksp-EIv1Y4qVhO5zvxo7m8s6ybUJqvqOz5u9Q");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rsa_4096_jwk() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_RSA_4096_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 5);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("e"));
 | 
				
			|||
	assert!(jwk.contains_key("n"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "RSA");
 | 
				
			|||
	assert_eq!(jwk.get("e").unwrap(), "AQAB");
 | 
				
			|||
	assert_eq!(jwk.get("n").unwrap(), "jm0Jh8Zk-aRqUxME0yLeHhBtdDFq6IvVesr_ffFwGqtlI4tIa6MqYuuOE5qlqoBrxt1fpccqGF82j47JqeoWerkNdZxYh1uKGQa9G2vB4OBOFHxYujT-NNPPlwnVPrp1_pjxK0hiOGzZRliNhO65lMbEueQCaLbQ6wstr4aVsWKiw8O4MG8Cc2ZegkWlYg0ZHOAn-uJlAt17NJfX8x576XRwX2KCssPYeyiwBukRne3ahVVqrc35EmV9JH3CxDaG9MeBLo7FUV5P1GNOIs313cbAgdMYV6Ahr2LvsaLucFvhSwmHh42--meDui-0wdFILjrz7MsITZy4xXjl8_zqMZ2dBaO38wP5o9VCfCXaVlD3IYB9A4-Y9IQ6wEVeodzGloPQyddUZ70kZ7O3E1kYu0tBKhoAL1VcKiUvOj1ABGooqf8QIAWCiAWqDqkZJ5HiR61NC9lJ3MH-aqYXzeHUuOxdlfr3oWPQKlRyYoUW93xD4dXjHQyHsIS4gWYE9ZJ9aIomNHj93UPNfR8ScxQTQBJV9Ttgl_vOUdmlasTmo0OXbTLMexzWCVISvUuoBoTcTY4DXCFeYHz3EE3ijJ9fnDkX3SsvV43x8X58py9rEyAQnRxfeahpWEoxW6TA5qBxg1GMBQu6CY2MupqsmI0XHTXzy8xCt4Pqd_a5zP6Wh_E");
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "RS256");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rsa_4096_jwk_thumbprint() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_RSA_4096_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 3);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("e"));
 | 
				
			|||
	assert!(jwk.contains_key("n"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "RSA");
 | 
				
			|||
	assert_eq!(jwk.get("e").unwrap(), "AQAB");
 | 
				
			|||
	assert_eq!(jwk.get("n").unwrap(), "jm0Jh8Zk-aRqUxME0yLeHhBtdDFq6IvVesr_ffFwGqtlI4tIa6MqYuuOE5qlqoBrxt1fpccqGF82j47JqeoWerkNdZxYh1uKGQa9G2vB4OBOFHxYujT-NNPPlwnVPrp1_pjxK0hiOGzZRliNhO65lMbEueQCaLbQ6wstr4aVsWKiw8O4MG8Cc2ZegkWlYg0ZHOAn-uJlAt17NJfX8x576XRwX2KCssPYeyiwBukRne3ahVVqrc35EmV9JH3CxDaG9MeBLo7FUV5P1GNOIs313cbAgdMYV6Ahr2LvsaLucFvhSwmHh42--meDui-0wdFILjrz7MsITZy4xXjl8_zqMZ2dBaO38wP5o9VCfCXaVlD3IYB9A4-Y9IQ6wEVeodzGloPQyddUZ70kZ7O3E1kYu0tBKhoAL1VcKiUvOj1ABGooqf8QIAWCiAWqDqkZJ5HiR61NC9lJ3MH-aqYXzeHUuOxdlfr3oWPQKlRyYoUW93xD4dXjHQyHsIS4gWYE9ZJ9aIomNHj93UPNfR8ScxQTQBJV9Ttgl_vOUdmlasTmo0OXbTLMexzWCVISvUuoBoTcTY4DXCFeYHz3EE3ijJ9fnDkX3SsvV43x8X58py9rEyAQnRxfeahpWEoxW6TA5qBxg1GMBQu6CY2MupqsmI0XHTXzy8xCt4Pqd_a5zP6Wh_E");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ecdsa_p256_jwk() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_P256_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 6);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("y"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "EC");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "P-256");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"VpJrz2a8rASzmbHStuDxNCjQc8ZiDnrGvVeRayNskrQ"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("y").unwrap(),
 | 
				
			|||
		"GrVCHhF5hN68efEgdoYS7acUT88qhMKQbULVcBgPBUg"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "ES256");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ecdsa_p256_jwk_thumbprint() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_P256_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 4);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("y"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "EC");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "P-256");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"VpJrz2a8rASzmbHStuDxNCjQc8ZiDnrGvVeRayNskrQ"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("y").unwrap(),
 | 
				
			|||
		"GrVCHhF5hN68efEgdoYS7acUT88qhMKQbULVcBgPBUg"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ecdsa_p384_jwk() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_P384_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 6);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("y"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "EC");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "P-384");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"N7TmS8prIp0DAGvwg1saML4UK61oe2PPJTeGLJt0iW-PMNcetFPcMF4WCa0ez80a"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("y").unwrap(),
 | 
				
			|||
		"RE5dtMDKV9Y8hsKf3fqLzMx75WORJaGswqC68xkRNjo0HcTar4tCB9VF9eSFfTMU"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "ES384");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ecdsa_p384_jwk_thumbprint() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_P384_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 4);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("y"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "EC");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "P-384");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"N7TmS8prIp0DAGvwg1saML4UK61oe2PPJTeGLJt0iW-PMNcetFPcMF4WCa0ez80a"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("y").unwrap(),
 | 
				
			|||
		"RE5dtMDKV9Y8hsKf3fqLzMx75WORJaGswqC68xkRNjo0HcTar4tCB9VF9eSFfTMU"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed25519_jwk() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_ED25519_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 5);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "OKP");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "Ed25519");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"DUX9ja8pq2wfkxuIaHzmhkdcVXMav_3rk5Y5ozOcp4o"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "EdDSA");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed25519_jwk_thumbprint() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_ED25519_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 3);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "OKP");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "Ed25519");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"DUX9ja8pq2wfkxuIaHzmhkdcVXMav_3rk5Y5ozOcp4o"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed25519_jwk_bis() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_ED25519_PEM_BIS.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 5);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "OKP");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "Ed25519");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"i9K0eV5qOJ_l_TWjWFLm8R-JbyGdlqFFeL_J0eEXFnc"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "EdDSA");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed25519_jwk_thumbprint_bis() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_ED25519_PEM_BIS.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 3);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "OKP");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "Ed25519");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"i9K0eV5qOJ_l_TWjWFLm8R-JbyGdlqFFeL_J0eEXFnc"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed448")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed448_jwk() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_ED448_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 5);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(jwk.contains_key("use"));
 | 
				
			|||
	assert!(jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "OKP");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "Ed448");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"b9GZ8b1hip3UMzkkNBdMF4JWBTZojxsNHK-jQBH94SY3boVs4Oeo291E1dGXz7RUMqIXjkSbU4EA"
 | 
				
			|||
	);
 | 
				
			|||
	assert_eq!(jwk.get("use").unwrap(), "sig");
 | 
				
			|||
	assert_eq!(jwk.get("alg").unwrap(), "EdDSA");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed448")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed448_jwk_thumbprint() {
 | 
				
			|||
	let k = KeyPair::from_pem(KEY_ECDSA_ED448_PEM.as_bytes()).unwrap();
 | 
				
			|||
	let jwk = k.jwk_public_key_thumbprint().unwrap();
 | 
				
			|||
	assert!(jwk.is_object());
 | 
				
			|||
	let jwk = jwk.as_object().unwrap();
 | 
				
			|||
	assert_eq!(jwk.len(), 3);
 | 
				
			|||
	assert!(jwk.contains_key("kty"));
 | 
				
			|||
	assert!(jwk.contains_key("crv"));
 | 
				
			|||
	assert!(jwk.contains_key("x"));
 | 
				
			|||
	assert!(!jwk.contains_key("use"));
 | 
				
			|||
	assert!(!jwk.contains_key("alg"));
 | 
				
			|||
	assert_eq!(jwk.get("kty").unwrap(), "OKP");
 | 
				
			|||
	assert_eq!(jwk.get("crv").unwrap(), "Ed448");
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		jwk.get("x").unwrap(),
 | 
				
			|||
		"b9GZ8b1hip3UMzkkNBdMF4JWBTZojxsNHK-jQBH94SY3boVs4Oeo291E1dGXz7RUMqIXjkSbU4EA"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
@ -1,344 +0,0 @@ | 
				
			|||
use crate::crypto::HashFunction;
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hash_from_str() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		("sha256", HashFunction::Sha256),
 | 
				
			|||
		("Sha256", HashFunction::Sha256),
 | 
				
			|||
		("sha-256", HashFunction::Sha256),
 | 
				
			|||
		("SHA_256", HashFunction::Sha256),
 | 
				
			|||
		("sha384", HashFunction::Sha384),
 | 
				
			|||
		("Sha-512", HashFunction::Sha512),
 | 
				
			|||
	];
 | 
				
			|||
	for (s, ref_h) in test_vectors {
 | 
				
			|||
		let h: HashFunction = s.parse().unwrap();
 | 
				
			|||
		assert_eq!(h, ref_h);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hash_from_invalid_str() {
 | 
				
			|||
	let test_vectors = vec!["sha42", "sha", "", "plop"];
 | 
				
			|||
	for s in test_vectors {
 | 
				
			|||
		let h = s.parse::<HashFunction>();
 | 
				
			|||
		assert!(h.is_err());
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hash_sha256() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		(
 | 
				
			|||
			"Hello World!".as_bytes(),
 | 
				
			|||
			vec![
 | 
				
			|||
				127, 131, 177, 101, 127, 241, 252, 83, 185, 45, 193, 129, 72, 161, 214, 93, 252,
 | 
				
			|||
				45, 75, 31, 163, 214, 119, 40, 74, 221, 210, 0, 18, 109, 144, 105,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			&[],
 | 
				
			|||
			vec![
 | 
				
			|||
				227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39,
 | 
				
			|||
				174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			&[
 | 
				
			|||
				194, 43, 6, 43, 252, 50, 206, 26, 240, 105, 85, 119, 40, 153, 213, 123, 158, 59, 8,
 | 
				
			|||
				45, 114,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				65, 72, 199, 76, 128, 174, 196, 223, 91, 235, 87, 119, 200, 212, 133, 13, 219, 223,
 | 
				
			|||
				60, 4, 73, 70, 65, 41, 226, 83, 221, 107, 112, 29, 205, 28,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
	];
 | 
				
			|||
	for (data, expected) in test_vectors {
 | 
				
			|||
		let h = HashFunction::Sha256;
 | 
				
			|||
		let res = h.hash(data);
 | 
				
			|||
		assert_eq!(res, expected);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hmac_sha256() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
 | 
				
			|||
			],
 | 
				
			|||
			vec![72, 105, 32, 84, 104, 101, 114, 101],
 | 
				
			|||
			vec![
 | 
				
			|||
				176, 52, 76, 97, 216, 219, 56, 83, 92, 168, 175, 206, 175, 11, 241, 43, 136, 29,
 | 
				
			|||
				194, 0, 201, 131, 61, 167, 38, 233, 55, 108, 46, 50, 207, 247,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![74, 101, 102, 101],
 | 
				
			|||
			vec![
 | 
				
			|||
				119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111,
 | 
				
			|||
				114, 32, 110, 111, 116, 104, 105, 110, 103, 63,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				91, 220, 193, 70, 191, 96, 117, 78, 106, 4, 36, 38, 8, 149, 117, 199, 90, 0, 63, 8,
 | 
				
			|||
				157, 39, 57, 131, 157, 236, 88, 185, 100, 236, 56, 67,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,
 | 
				
			|||
				170, 170, 170, 170,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				119, 62, 169, 30, 54, 128, 14, 70, 133, 77, 184, 235, 208, 145, 129, 167, 41, 89,
 | 
				
			|||
				9, 139, 62, 248, 193, 34, 217, 99, 85, 20, 206, 213, 101, 254,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
 | 
				
			|||
				24, 25,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				130, 85, 138, 56, 154, 68, 60, 14, 164, 204, 129, 152, 153, 242, 8, 58, 133, 240,
 | 
				
			|||
				250, 163, 229, 120, 248, 7, 122, 46, 63, 244, 103, 41, 102, 91,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
	];
 | 
				
			|||
	for (key, data, expected) in test_vectors {
 | 
				
			|||
		let h = HashFunction::Sha256;
 | 
				
			|||
		let res = h.hmac(&key, &data).unwrap();
 | 
				
			|||
		assert_eq!(res, expected);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hash_sha384() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		(
 | 
				
			|||
			"Hello World!".as_bytes(),
 | 
				
			|||
			vec![
 | 
				
			|||
				191, 215, 108, 14, 187, 208, 6, 254, 229, 131, 65, 5, 71, 193, 136, 123, 2, 146,
 | 
				
			|||
				190, 118, 213, 130, 217, 108, 36, 45, 42, 121, 39, 35, 227, 253, 111, 208, 97, 249,
 | 
				
			|||
				213, 207, 209, 59, 143, 150, 19, 88, 230, 173, 186, 74,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			&[],
 | 
				
			|||
			vec![
 | 
				
			|||
				56, 176, 96, 167, 81, 172, 150, 56, 76, 217, 50, 126, 177, 177, 227, 106, 33, 253,
 | 
				
			|||
				183, 17, 20, 190, 7, 67, 76, 12, 199, 191, 99, 246, 225, 218, 39, 78, 222, 191,
 | 
				
			|||
				231, 111, 101, 251, 213, 26, 210, 241, 72, 152, 185, 91,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			&[
 | 
				
			|||
				194, 43, 6, 43, 252, 50, 206, 26, 240, 105, 85, 119, 40, 153, 213, 123, 158, 59, 8,
 | 
				
			|||
				45, 114,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				170, 126, 84, 2, 141, 91, 106, 70, 80, 53, 98, 101, 184, 3, 34, 146, 130, 238, 146,
 | 
				
			|||
				221, 113, 197, 154, 91, 4, 208, 229, 15, 8, 179, 51, 29, 224, 200, 187, 127, 9,
 | 
				
			|||
				243, 29, 171, 189, 124, 60, 39, 3, 74, 171, 156,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
	];
 | 
				
			|||
	for (data, expected) in test_vectors {
 | 
				
			|||
		let h = HashFunction::Sha384;
 | 
				
			|||
		let res = h.hash(data);
 | 
				
			|||
		assert_eq!(res, expected);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hmac_sha384() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
 | 
				
			|||
			],
 | 
				
			|||
			vec![72, 105, 32, 84, 104, 101, 114, 101],
 | 
				
			|||
			vec![
 | 
				
			|||
				175, 208, 57, 68, 216, 72, 149, 98, 107, 8, 37, 244, 171, 70, 144, 127, 21, 249,
 | 
				
			|||
				218, 219, 228, 16, 30, 198, 130, 170, 3, 76, 124, 235, 197, 156, 250, 234, 158,
 | 
				
			|||
				169, 7, 110, 222, 127, 74, 241, 82, 232, 178, 250, 156, 182,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![74, 101, 102, 101],
 | 
				
			|||
			vec![
 | 
				
			|||
				119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111,
 | 
				
			|||
				114, 32, 110, 111, 116, 104, 105, 110, 103, 63,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				175, 69, 210, 227, 118, 72, 64, 49, 97, 127, 120, 210, 181, 138, 107, 27, 156, 126,
 | 
				
			|||
				244, 100, 245, 160, 27, 71, 228, 46, 195, 115, 99, 34, 68, 94, 142, 34, 64, 202,
 | 
				
			|||
				94, 105, 226, 199, 139, 50, 57, 236, 250, 178, 22, 73,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,
 | 
				
			|||
				170, 170, 170, 170,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				136, 6, 38, 8, 211, 230, 173, 138, 10, 162, 172, 224, 20, 200, 168, 111, 10, 166,
 | 
				
			|||
				53, 217, 71, 172, 159, 235, 232, 62, 244, 229, 89, 102, 20, 75, 42, 90, 179, 157,
 | 
				
			|||
				193, 56, 20, 185, 78, 58, 182, 225, 1, 163, 79, 39,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
 | 
				
			|||
				24, 25,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				62, 138, 105, 183, 120, 60, 37, 133, 25, 51, 171, 98, 144, 175, 108, 167, 122, 153,
 | 
				
			|||
				129, 72, 8, 80, 0, 156, 197, 87, 124, 110, 31, 87, 59, 78, 104, 1, 221, 35, 196,
 | 
				
			|||
				167, 214, 121, 204, 248, 163, 134, 198, 116, 207, 251,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
	];
 | 
				
			|||
	for (key, data, expected) in test_vectors {
 | 
				
			|||
		let h = HashFunction::Sha384;
 | 
				
			|||
		let res = h.hmac(&key, &data).unwrap();
 | 
				
			|||
		assert_eq!(res, expected);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hash_sha512() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		(
 | 
				
			|||
			"Hello World!".as_bytes(),
 | 
				
			|||
			vec![
 | 
				
			|||
				134, 24, 68, 214, 112, 78, 133, 115, 254, 195, 77, 150, 126, 32, 188, 254, 243,
 | 
				
			|||
				212, 36, 207, 72, 190, 4, 230, 220, 8, 242, 189, 88, 199, 41, 116, 51, 113, 1, 94,
 | 
				
			|||
				173, 137, 28, 195, 207, 28, 157, 52, 180, 146, 100, 181, 16, 117, 27, 31, 249, 229,
 | 
				
			|||
				55, 147, 123, 196, 107, 93, 111, 244, 236, 200,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			&[],
 | 
				
			|||
			vec![
 | 
				
			|||
				207, 131, 225, 53, 126, 239, 184, 189, 241, 84, 40, 80, 214, 109, 128, 7, 214, 32,
 | 
				
			|||
				228, 5, 11, 87, 21, 220, 131, 244, 169, 33, 211, 108, 233, 206, 71, 208, 209, 60,
 | 
				
			|||
				93, 133, 242, 176, 255, 131, 24, 210, 135, 126, 236, 47, 99, 185, 49, 189, 71, 65,
 | 
				
			|||
				122, 129, 165, 56, 50, 122, 249, 39, 218, 62,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			&[
 | 
				
			|||
				194, 43, 6, 43, 252, 50, 206, 26, 240, 105, 85, 119, 40, 153, 213, 123, 158, 59, 8,
 | 
				
			|||
				45, 114,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				58, 93, 210, 174, 119, 179, 246, 25, 14, 148, 182, 109, 28, 14, 16, 80, 45, 231,
 | 
				
			|||
				104, 169, 130, 43, 39, 221, 12, 112, 85, 159, 123, 6, 227, 35, 61, 24, 158, 190,
 | 
				
			|||
				162, 11, 247, 204, 98, 41, 242, 5, 52, 116, 149, 220, 124, 82, 159, 181, 74, 210,
 | 
				
			|||
				85, 190, 59, 130, 209, 8, 181, 247, 192, 65,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
	];
 | 
				
			|||
	for (data, expected) in test_vectors {
 | 
				
			|||
		let h = HashFunction::Sha512;
 | 
				
			|||
		let res = h.hash(data);
 | 
				
			|||
		assert_eq!(res, expected);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_hmac_sha512() {
 | 
				
			|||
	let test_vectors = vec![
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
 | 
				
			|||
			],
 | 
				
			|||
			vec![72, 105, 32, 84, 104, 101, 114, 101],
 | 
				
			|||
			vec![
 | 
				
			|||
				135, 170, 124, 222, 165, 239, 97, 157, 79, 240, 180, 36, 26, 29, 108, 176, 35, 121,
 | 
				
			|||
				244, 226, 206, 78, 194, 120, 122, 208, 179, 5, 69, 225, 124, 222, 218, 168, 51,
 | 
				
			|||
				183, 214, 184, 167, 2, 3, 139, 39, 78, 174, 163, 244, 228, 190, 157, 145, 78, 235,
 | 
				
			|||
				97, 241, 112, 46, 105, 108, 32, 58, 18, 104, 84,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![74, 101, 102, 101],
 | 
				
			|||
			vec![
 | 
				
			|||
				119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111,
 | 
				
			|||
				114, 32, 110, 111, 116, 104, 105, 110, 103, 63,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				22, 75, 122, 123, 252, 248, 25, 226, 227, 149, 251, 231, 59, 86, 224, 163, 135,
 | 
				
			|||
				189, 100, 34, 46, 131, 31, 214, 16, 39, 12, 215, 234, 37, 5, 84, 151, 88, 191, 117,
 | 
				
			|||
				192, 90, 153, 74, 109, 3, 79, 101, 248, 240, 230, 253, 202, 234, 177, 163, 77, 74,
 | 
				
			|||
				107, 75, 99, 110, 7, 10, 56, 188, 231, 55,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,
 | 
				
			|||
				170, 170, 170, 170,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
 | 
				
			|||
				221, 221,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				250, 115, 176, 8, 157, 86, 162, 132, 239, 176, 240, 117, 108, 137, 11, 233, 177,
 | 
				
			|||
				181, 219, 221, 142, 232, 26, 54, 85, 248, 62, 51, 178, 39, 157, 57, 191, 62, 132,
 | 
				
			|||
				130, 121, 167, 34, 200, 6, 180, 133, 164, 126, 103, 200, 7, 185, 70, 163, 55, 190,
 | 
				
			|||
				232, 148, 38, 116, 39, 136, 89, 225, 50, 146, 251,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
		(
 | 
				
			|||
			vec![
 | 
				
			|||
				1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
 | 
				
			|||
				24, 25,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
 | 
				
			|||
				205, 205,
 | 
				
			|||
			],
 | 
				
			|||
			vec![
 | 
				
			|||
				176, 186, 70, 86, 55, 69, 140, 105, 144, 229, 168, 197, 246, 29, 74, 247, 229, 118,
 | 
				
			|||
				217, 127, 249, 75, 135, 45, 231, 111, 128, 80, 54, 30, 227, 219, 169, 28, 165, 193,
 | 
				
			|||
				26, 162, 94, 180, 214, 121, 39, 92, 197, 120, 128, 99, 165, 241, 151, 65, 18, 12,
 | 
				
			|||
				79, 45, 226, 173, 235, 235, 16, 162, 152, 221,
 | 
				
			|||
			],
 | 
				
			|||
		),
 | 
				
			|||
	];
 | 
				
			|||
	for (key, data, expected) in test_vectors {
 | 
				
			|||
		let h = HashFunction::Sha512;
 | 
				
			|||
		let res = h.hmac(&key, &data).unwrap();
 | 
				
			|||
		assert_eq!(res, expected);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,42 +0,0 @@ | 
				
			|||
use crate::to_idna;
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_no_idna() {
 | 
				
			|||
	let idna_res = to_idna("HeLo.example.com");
 | 
				
			|||
	assert!(idna_res.is_ok());
 | 
				
			|||
	assert_eq!(idna_res.unwrap(), "helo.example.com");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_simple_idna() {
 | 
				
			|||
	let idna_res = to_idna("Hélo.Example.com");
 | 
				
			|||
	assert!(idna_res.is_ok());
 | 
				
			|||
	assert_eq!(idna_res.unwrap(), "xn--hlo-bma.example.com");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_multiple_idna() {
 | 
				
			|||
	let idna_res = to_idna("ns1.hÉlo.aç-éièè.example.com");
 | 
				
			|||
	assert!(idna_res.is_ok());
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		idna_res.unwrap(),
 | 
				
			|||
		"ns1.xn--hlo-bma.xn--a-i-2lahae.example.com"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_already_idna() {
 | 
				
			|||
	let idna_res = to_idna("xn--hlo-bma.example.com");
 | 
				
			|||
	assert!(idna_res.is_ok());
 | 
				
			|||
	assert_eq!(idna_res.unwrap(), "xn--hlo-bma.example.com");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_mixed_idna_parts() {
 | 
				
			|||
	let idna_res = to_idna("ns1.xn--hlo-bma.aç-éièè.example.com");
 | 
				
			|||
	assert!(idna_res.is_ok());
 | 
				
			|||
	assert_eq!(
 | 
				
			|||
		idna_res.unwrap(),
 | 
				
			|||
		"ns1.xn--hlo-bma.xn--a-i-2lahae.example.com"
 | 
				
			|||
	);
 | 
				
			|||
}
 | 
				
			|||
@ -1,62 +0,0 @@ | 
				
			|||
use crate::crypto::{gen_keypair, JwsSignatureAlgorithm, KeyType};
 | 
				
			|||
 | 
				
			|||
const TEST_DATA: &'static [u8] = &[72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33];
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rs256_sign_rsa2048() {
 | 
				
			|||
	let k = gen_keypair(KeyType::Rsa2048).unwrap();
 | 
				
			|||
	let _ = k.sign(&JwsSignatureAlgorithm::Rs256, TEST_DATA).unwrap();
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rs256_sign_rsa4096() {
 | 
				
			|||
	let k = gen_keypair(KeyType::Rsa4096).unwrap();
 | 
				
			|||
	let _ = k.sign(&JwsSignatureAlgorithm::Rs256, TEST_DATA).unwrap();
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_rs256_sign_ecdsa() {
 | 
				
			|||
	let k = gen_keypair(KeyType::EcdsaP256).unwrap();
 | 
				
			|||
	let res = k.sign(&JwsSignatureAlgorithm::Rs256, TEST_DATA);
 | 
				
			|||
	assert!(res.is_err());
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_es256_sign_p256() {
 | 
				
			|||
	let k = gen_keypair(KeyType::EcdsaP256).unwrap();
 | 
				
			|||
	let _ = k.sign(&JwsSignatureAlgorithm::Es256, TEST_DATA).unwrap();
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_es256_sign_p384() {
 | 
				
			|||
	let k = gen_keypair(KeyType::EcdsaP384).unwrap();
 | 
				
			|||
	let res = k.sign(&JwsSignatureAlgorithm::Es256, TEST_DATA);
 | 
				
			|||
	assert!(res.is_err());
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_es384_sign_p384() {
 | 
				
			|||
	let k = gen_keypair(KeyType::EcdsaP384).unwrap();
 | 
				
			|||
	let _ = k.sign(&JwsSignatureAlgorithm::Es384, TEST_DATA).unwrap();
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_es384_sign_p256() {
 | 
				
			|||
	let k = gen_keypair(KeyType::EcdsaP256).unwrap();
 | 
				
			|||
	let res = k.sign(&JwsSignatureAlgorithm::Es384, TEST_DATA);
 | 
				
			|||
	assert!(res.is_err());
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed25519")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed25519_sign() {
 | 
				
			|||
	let k = gen_keypair(KeyType::Ed25519).unwrap();
 | 
				
			|||
	let _ = k.sign(&JwsSignatureAlgorithm::Ed25519, TEST_DATA).unwrap();
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "ed448")]
 | 
				
			|||
#[test]
 | 
				
			|||
fn test_ed448_sign() {
 | 
				
			|||
	let k = gen_keypair(KeyType::Ed448).unwrap();
 | 
				
			|||
	let _ = k.sign(&JwsSignatureAlgorithm::Ed448, TEST_DATA).unwrap();
 | 
				
			|||
}
 | 
				
			|||
@ -1,46 +0,0 @@ | 
				
			|||
[package] | 
				
			|||
name = "acmed" | 
				
			|||
version = "0.24.0" | 
				
			|||
authors = ["Rodolphe Breard <rodolphe@what.tf>"] | 
				
			|||
edition = "2018" | 
				
			|||
description = "ACME (RFC 8555) client daemon" | 
				
			|||
readme = "../README.md" | 
				
			|||
repository = "https://github.com/breard-r/acmed" | 
				
			|||
license = "MIT OR Apache-2.0" | 
				
			|||
keywords = ["acme", "tls", "X.509"] | 
				
			|||
categories = ["cryptography"] | 
				
			|||
build = "build.rs" | 
				
			|||
include = ["src/**/*", "Cargo.toml", "LICENSE-*.txt"] | 
				
			|||
publish = false | 
				
			|||
rust-version = "1.74.0" | 
				
			|||
 | 
				
			|||
[features] | 
				
			|||
default = ["openssl_dyn"] | 
				
			|||
crypto_openssl = [] | 
				
			|||
openssl_dyn = ["crypto_openssl", "acme_common/openssl_dyn"] | 
				
			|||
openssl_vendored = ["crypto_openssl", "acme_common/openssl_vendored"] | 
				
			|||
 | 
				
			|||
[dependencies] | 
				
			|||
acme_common = { path = "../acme_common" } | 
				
			|||
async-lock = "3.3.0" | 
				
			|||
async-process = "2.1.0" | 
				
			|||
bincode = "1.3.3" | 
				
			|||
clap = { version = "4.5.3", features = ["string"] } | 
				
			|||
futures = "0.3.30" | 
				
			|||
glob = "0.3.1" | 
				
			|||
log = "0.4.21" | 
				
			|||
nom = { version = "7.1.3", default-features = false, features = [] } | 
				
			|||
serde = { version = "1.0.197", features = ["derive"] } | 
				
			|||
serde_json = "1.0.114" | 
				
			|||
toml = "0.8.12" | 
				
			|||
tokio = { version = "1.36.0", features = ["full"] } | 
				
			|||
rand = "0.8.5" | 
				
			|||
reqwest = "0.12.1" | 
				
			|||
minijinja = "2.5.0" | 
				
			|||
 | 
				
			|||
[target.'cfg(unix)'.dependencies] | 
				
			|||
nix = { version = "0.29.0", features = ["fs", "user"] } | 
				
			|||
 | 
				
			|||
[build-dependencies] | 
				
			|||
serde = { version = "1.0.197", features = ["derive"] } | 
				
			|||
toml = "0.8.12" | 
				
			|||
@ -1,130 +0,0 @@ | 
				
			|||
extern crate serde;
 | 
				
			|||
extern crate toml;
 | 
				
			|||
 | 
				
			|||
use serde::Deserialize;
 | 
				
			|||
use std::env;
 | 
				
			|||
use std::fs::File;
 | 
				
			|||
use std::io::prelude::*;
 | 
				
			|||
use std::path::PathBuf;
 | 
				
			|||
 | 
				
			|||
macro_rules! set_rustc_env_var {
 | 
				
			|||
	($name: expr, $value: expr) => {{
 | 
				
			|||
		println!("cargo:rustc-env={}={}", $name, $value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_env_var_if_absent {
 | 
				
			|||
	($name: expr, $default_value: expr) => {{
 | 
				
			|||
		if let Err(_) = env::var($name) {
 | 
				
			|||
			set_rustc_env_var!($name, $default_value);
 | 
				
			|||
		}
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_specific_path_if_absent {
 | 
				
			|||
	($env_name: expr, $env_default: expr, $with_dir: expr, $name: expr, $default_value: expr) => {{
 | 
				
			|||
		let prefix = env::var($env_name).unwrap_or(String::from($env_default));
 | 
				
			|||
		let mut value = PathBuf::new();
 | 
				
			|||
		value.push(prefix);
 | 
				
			|||
		if ($with_dir) {
 | 
				
			|||
			value.push("acmed");
 | 
				
			|||
		}
 | 
				
			|||
		value.push($default_value);
 | 
				
			|||
		set_env_var_if_absent!($name, value.to_str().unwrap());
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_data_path_if_absent {
 | 
				
			|||
	($name: expr, $default_value: expr) => {{
 | 
				
			|||
		set_specific_path_if_absent!("VARLIBDIR", "/var/lib", true, $name, $default_value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_cfg_path_if_absent {
 | 
				
			|||
	($name: expr, $default_value: expr) => {{
 | 
				
			|||
		set_specific_path_if_absent!("SYSCONFDIR", "/etc", true, $name, $default_value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_runstate_path_if_absent {
 | 
				
			|||
	($name: expr, $default_value: expr) => {{
 | 
				
			|||
		set_specific_path_if_absent!("RUNSTATEDIR", "/run", false, $name, $default_value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
pub struct Lock {
 | 
				
			|||
	package: Vec<Package>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
struct Package {
 | 
				
			|||
	name: String,
 | 
				
			|||
	version: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
struct Error;
 | 
				
			|||
 | 
				
			|||
impl From<std::io::Error> for Error {
 | 
				
			|||
	fn from(_error: std::io::Error) -> Self {
 | 
				
			|||
		Error {}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<toml::de::Error> for Error {
 | 
				
			|||
	fn from(_error: toml::de::Error) -> Self {
 | 
				
			|||
		Error {}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_lock() -> Result<Lock, Error> {
 | 
				
			|||
	let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
 | 
				
			|||
	path.pop();
 | 
				
			|||
	path.push("Cargo.lock");
 | 
				
			|||
	let mut file = File::open(path)?;
 | 
				
			|||
	let mut contents = String::new();
 | 
				
			|||
	file.read_to_string(&mut contents)?;
 | 
				
			|||
	let ret: Lock = toml::from_str(&contents)?;
 | 
				
			|||
	Ok(ret)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn set_lock() {
 | 
				
			|||
	let lock = match get_lock() {
 | 
				
			|||
		Ok(l) => l,
 | 
				
			|||
		Err(_) => {
 | 
				
			|||
			return;
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	for p in lock.package.iter() {
 | 
				
			|||
		if p.name == "reqwest" {
 | 
				
			|||
			let agent = format!("{}/{}", p.name, p.version);
 | 
				
			|||
			set_rustc_env_var!("ACMED_HTTP_LIB_AGENT", agent);
 | 
				
			|||
			set_rustc_env_var!("ACMED_HTTP_LIB_NAME", p.name);
 | 
				
			|||
			set_rustc_env_var!("ACMED_HTTP_LIB_VERSION", p.version);
 | 
				
			|||
			return;
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn set_target() {
 | 
				
			|||
	if let Ok(target) = env::var("TARGET") {
 | 
				
			|||
		set_rustc_env_var!("ACMED_TARGET", target);
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn set_default_values() {
 | 
				
			|||
	set_data_path_if_absent!("ACMED_DEFAULT_ACCOUNTS_DIR", "accounts");
 | 
				
			|||
	set_data_path_if_absent!("ACMED_DEFAULT_CERT_DIR", "certs");
 | 
				
			|||
	set_env_var_if_absent!(
 | 
				
			|||
		"ACMED_DEFAULT_CERT_FORMAT",
 | 
				
			|||
		"{{ name }}_{{ key_type }}.{{ file_type }}.{{ ext }}"
 | 
				
			|||
	);
 | 
				
			|||
	set_cfg_path_if_absent!("ACMED_DEFAULT_CONFIG_FILE", "acmed.toml");
 | 
				
			|||
	set_runstate_path_if_absent!("ACMED_DEFAULT_PID_FILE", "acmed.pid");
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn main() {
 | 
				
			|||
	set_target();
 | 
				
			|||
	set_lock();
 | 
				
			|||
	set_default_values();
 | 
				
			|||
}
 | 
				
			|||
@ -1,319 +0,0 @@ | 
				
			|||
use crate::acme_proto::account::{register_account, update_account_contacts, update_account_key};
 | 
				
			|||
use crate::endpoint::Endpoint;
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::storage::FileManager;
 | 
				
			|||
use acme_common::crypto::{gen_keypair, HashFunction, JwsSignatureAlgorithm, KeyPair, KeyType};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use std::collections::HashMap;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
use std::time::SystemTime;
 | 
				
			|||
 | 
				
			|||
mod contact;
 | 
				
			|||
mod storage;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct ExternalAccount {
 | 
				
			|||
	pub identifier: String,
 | 
				
			|||
	pub key: Vec<u8>,
 | 
				
			|||
	pub signature_algorithm: JwsSignatureAlgorithm,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub enum AccountContactType {
 | 
				
			|||
	Mailfrom,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl FromStr for AccountContactType {
 | 
				
			|||
	type Err = Error;
 | 
				
			|||
 | 
				
			|||
	fn from_str(s: &str) -> Result<Self, Error> {
 | 
				
			|||
		match s.to_lowercase().as_str() {
 | 
				
			|||
			"mailfrom" => Ok(AccountContactType::Mailfrom),
 | 
				
			|||
			_ => Err(format!("{s}: unknown contact type.").into()),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for AccountContactType {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			AccountContactType::Mailfrom => "mailfrom",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct AccountKey {
 | 
				
			|||
	pub creation_date: SystemTime,
 | 
				
			|||
	pub key: KeyPair,
 | 
				
			|||
	pub signature_algorithm: JwsSignatureAlgorithm,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountKey {
 | 
				
			|||
	fn new(key_type: KeyType, signature_algorithm: JwsSignatureAlgorithm) -> Result<Self, Error> {
 | 
				
			|||
		Ok(AccountKey {
 | 
				
			|||
			creation_date: SystemTime::now(),
 | 
				
			|||
			key: gen_keypair(key_type)?,
 | 
				
			|||
			signature_algorithm,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Hash)]
 | 
				
			|||
pub struct AccountEndpoint {
 | 
				
			|||
	pub creation_date: SystemTime,
 | 
				
			|||
	pub account_url: String,
 | 
				
			|||
	pub orders_url: String,
 | 
				
			|||
	pub key_hash: Vec<u8>,
 | 
				
			|||
	pub contacts_hash: Vec<u8>,
 | 
				
			|||
	pub external_account_hash: Vec<u8>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountEndpoint {
 | 
				
			|||
	pub fn new() -> Self {
 | 
				
			|||
		AccountEndpoint {
 | 
				
			|||
			creation_date: SystemTime::UNIX_EPOCH,
 | 
				
			|||
			account_url: String::new(),
 | 
				
			|||
			orders_url: String::new(),
 | 
				
			|||
			key_hash: Vec::new(),
 | 
				
			|||
			contacts_hash: Vec::new(),
 | 
				
			|||
			external_account_hash: Vec::new(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct Account {
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub endpoints: HashMap<String, AccountEndpoint>,
 | 
				
			|||
	pub contacts: Vec<contact::AccountContact>,
 | 
				
			|||
	pub current_key: AccountKey,
 | 
				
			|||
	pub past_keys: Vec<AccountKey>,
 | 
				
			|||
	pub file_manager: FileManager,
 | 
				
			|||
	pub external_account: Option<ExternalAccount>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl HasLogger for Account {
 | 
				
			|||
	fn warn(&self, msg: &str) {
 | 
				
			|||
		log::warn!("account \"{}\": {msg}", &self.name);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn info(&self, msg: &str) {
 | 
				
			|||
		log::info!("account \"{}\": {msg}", &self.name);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn debug(&self, msg: &str) {
 | 
				
			|||
		log::debug!("account \"{}\": {msg}", &self.name);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn trace(&self, msg: &str) {
 | 
				
			|||
		log::trace!("account \"{}\": {msg}", &self.name);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Account {
 | 
				
			|||
	pub fn get_endpoint_mut(&mut self, endpoint_name: &str) -> Result<&mut AccountEndpoint, Error> {
 | 
				
			|||
		match self.endpoints.get_mut(endpoint_name) {
 | 
				
			|||
			Some(ep) => Ok(ep),
 | 
				
			|||
			None => {
 | 
				
			|||
				let msg = format!(
 | 
				
			|||
					"\"{}\": unknown endpoint for account \"{}\"",
 | 
				
			|||
					endpoint_name, self.name
 | 
				
			|||
				);
 | 
				
			|||
				Err(msg.into())
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_endpoint(&self, endpoint_name: &str) -> Result<&AccountEndpoint, Error> {
 | 
				
			|||
		match self.endpoints.get(endpoint_name) {
 | 
				
			|||
			Some(ep) => Ok(ep),
 | 
				
			|||
			None => {
 | 
				
			|||
				let msg = format!(
 | 
				
			|||
					"\"{}\": unknown endpoint for account \"{}\"",
 | 
				
			|||
					endpoint_name, self.name
 | 
				
			|||
				);
 | 
				
			|||
				Err(msg.into())
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_past_key(&self, key_hash: &[u8]) -> Result<&AccountKey, Error> {
 | 
				
			|||
		let key_hash = key_hash.to_vec();
 | 
				
			|||
		for key in &self.past_keys {
 | 
				
			|||
			let past_key_hash = hash_key(key)?;
 | 
				
			|||
			if past_key_hash == key_hash {
 | 
				
			|||
				return Ok(key);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		Err("key not found".into())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn load(
 | 
				
			|||
		file_manager: &FileManager,
 | 
				
			|||
		name: &str,
 | 
				
			|||
		contacts: &[(String, String)],
 | 
				
			|||
		key_type: &Option<String>,
 | 
				
			|||
		signature_algorithm: &Option<String>,
 | 
				
			|||
		external_account: &Option<ExternalAccount>,
 | 
				
			|||
	) -> Result<Self, Error> {
 | 
				
			|||
		let contacts = contacts
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|(k, v)| contact::AccountContact::new(k, v))
 | 
				
			|||
			.collect::<Result<Vec<contact::AccountContact>, Error>>()?;
 | 
				
			|||
		let key_type = match key_type {
 | 
				
			|||
			Some(kt) => kt.parse()?,
 | 
				
			|||
			None => crate::DEFAULT_ACCOUNT_KEY_TYPE,
 | 
				
			|||
		};
 | 
				
			|||
		let signature_algorithm = match signature_algorithm {
 | 
				
			|||
			Some(sa) => sa.parse()?,
 | 
				
			|||
			None => key_type.get_default_signature_alg(),
 | 
				
			|||
		};
 | 
				
			|||
		key_type.check_alg_compatibility(&signature_algorithm)?;
 | 
				
			|||
		let account = match storage::fetch(file_manager, name).await? {
 | 
				
			|||
			Some(mut a) => {
 | 
				
			|||
				a.update_keys(key_type, signature_algorithm).await?;
 | 
				
			|||
				a.contacts = contacts;
 | 
				
			|||
				a.external_account = external_account.to_owned();
 | 
				
			|||
				a
 | 
				
			|||
			}
 | 
				
			|||
			None => {
 | 
				
			|||
				let account = Account {
 | 
				
			|||
					name: name.to_string(),
 | 
				
			|||
					endpoints: HashMap::new(),
 | 
				
			|||
					contacts,
 | 
				
			|||
					current_key: AccountKey::new(key_type, signature_algorithm)?,
 | 
				
			|||
					past_keys: Vec::new(),
 | 
				
			|||
					file_manager: file_manager.clone(),
 | 
				
			|||
					external_account: external_account.to_owned(),
 | 
				
			|||
				};
 | 
				
			|||
				account.debug("initializing a new account");
 | 
				
			|||
				account
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		Ok(account)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn add_endpoint_name(&mut self, endpoint_name: &str) {
 | 
				
			|||
		self.endpoints
 | 
				
			|||
			.entry(endpoint_name.to_string())
 | 
				
			|||
			.or_insert_with(AccountEndpoint::new);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn synchronize(&mut self, endpoint: &mut Endpoint) -> Result<(), Error> {
 | 
				
			|||
		let acc_ep = self.get_endpoint(&endpoint.name)?;
 | 
				
			|||
		if !acc_ep.account_url.is_empty() {
 | 
				
			|||
			if let Some(ec) = &self.external_account {
 | 
				
			|||
				let external_account_hash = hash_external_account(ec);
 | 
				
			|||
				if external_account_hash != acc_ep.external_account_hash {
 | 
				
			|||
					let msg = format!(
 | 
				
			|||
						"external account changed on endpoint \"{}\"",
 | 
				
			|||
						&endpoint.name
 | 
				
			|||
					);
 | 
				
			|||
					self.info(&msg);
 | 
				
			|||
					register_account(endpoint, self).await?;
 | 
				
			|||
					return Ok(());
 | 
				
			|||
				}
 | 
				
			|||
			}
 | 
				
			|||
			let ct_hash = hash_contacts(&self.contacts);
 | 
				
			|||
			let key_hash = hash_key(&self.current_key)?;
 | 
				
			|||
			let contacts_changed = ct_hash != acc_ep.contacts_hash;
 | 
				
			|||
			let key_changed = key_hash != acc_ep.key_hash;
 | 
				
			|||
			if contacts_changed {
 | 
				
			|||
				update_account_contacts(endpoint, self).await?;
 | 
				
			|||
			}
 | 
				
			|||
			if key_changed {
 | 
				
			|||
				update_account_key(endpoint, self).await?;
 | 
				
			|||
			}
 | 
				
			|||
		} else {
 | 
				
			|||
			register_account(endpoint, self).await?;
 | 
				
			|||
		}
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn register(&mut self, endpoint: &mut Endpoint) -> Result<(), Error> {
 | 
				
			|||
		register_account(endpoint, self).await
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn save(&self) -> Result<(), Error> {
 | 
				
			|||
		storage::save(&self.file_manager, self).await
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn set_account_url(&mut self, endpoint_name: &str, account_url: &str) -> Result<(), Error> {
 | 
				
			|||
		let ep = self.get_endpoint_mut(endpoint_name)?;
 | 
				
			|||
		ep.account_url = account_url.to_string();
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn set_orders_url(&mut self, endpoint_name: &str, orders_url: &str) -> Result<(), Error> {
 | 
				
			|||
		let ep = self.get_endpoint_mut(endpoint_name)?;
 | 
				
			|||
		ep.orders_url = orders_url.to_string();
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn update_key_hash(&mut self, endpoint_name: &str) -> Result<(), Error> {
 | 
				
			|||
		let key = self.current_key.clone();
 | 
				
			|||
		let ep = self.get_endpoint_mut(endpoint_name)?;
 | 
				
			|||
		ep.key_hash = hash_key(&key)?;
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn update_contacts_hash(&mut self, endpoint_name: &str) -> Result<(), Error> {
 | 
				
			|||
		let ct = self.contacts.clone();
 | 
				
			|||
		let ep = self.get_endpoint_mut(endpoint_name)?;
 | 
				
			|||
		ep.contacts_hash = hash_contacts(&ct);
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn update_external_account_hash(&mut self, endpoint_name: &str) -> Result<(), Error> {
 | 
				
			|||
		if let Some(ec) = &self.external_account {
 | 
				
			|||
			let ec = ec.clone();
 | 
				
			|||
			let ep = self.get_endpoint_mut(endpoint_name)?;
 | 
				
			|||
			ep.external_account_hash = hash_external_account(&ec);
 | 
				
			|||
		}
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	async fn update_keys(
 | 
				
			|||
		&mut self,
 | 
				
			|||
		key_type: KeyType,
 | 
				
			|||
		signature_algorithm: JwsSignatureAlgorithm,
 | 
				
			|||
	) -> Result<(), Error> {
 | 
				
			|||
		if self.current_key.key.key_type != key_type
 | 
				
			|||
			|| self.current_key.signature_algorithm != signature_algorithm
 | 
				
			|||
		{
 | 
				
			|||
			self.debug("account key has been changed in the configuration, creating a new one...");
 | 
				
			|||
			self.past_keys.push(self.current_key.to_owned());
 | 
				
			|||
			self.current_key = AccountKey::new(key_type, signature_algorithm)?;
 | 
				
			|||
			self.save().await?;
 | 
				
			|||
			let msg = format!("new {key_type} account key created, using {signature_algorithm} as signing algorithm");
 | 
				
			|||
			self.info(&msg);
 | 
				
			|||
		} else {
 | 
				
			|||
			self.trace("account key is up to date");
 | 
				
			|||
		}
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn hash_contacts(contacts: &[contact::AccountContact]) -> Vec<u8> {
 | 
				
			|||
	let msg = contacts
 | 
				
			|||
		.iter()
 | 
				
			|||
		.map(|v| v.to_string())
 | 
				
			|||
		.collect::<Vec<String>>()
 | 
				
			|||
		.join("")
 | 
				
			|||
		.into_bytes();
 | 
				
			|||
	HashFunction::Sha256.hash(&msg)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn hash_key(key: &AccountKey) -> Result<Vec<u8>, Error> {
 | 
				
			|||
	let pem = key.key.public_key_to_pem()?;
 | 
				
			|||
	Ok(HashFunction::Sha256.hash(&pem))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn hash_external_account(ec: &ExternalAccount) -> Vec<u8> {
 | 
				
			|||
	let mut msg = ec.key.clone();
 | 
				
			|||
	msg.extend(ec.identifier.as_bytes());
 | 
				
			|||
	HashFunction::Sha256.hash(&msg)
 | 
				
			|||
}
 | 
				
			|||
@ -1,110 +0,0 @@ | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
fn clean_mailto(value: &str) -> Result<String, Error> {
 | 
				
			|||
	// TODO: implement a simple RFC 6068 parser
 | 
				
			|||
	//  - no "hfields"
 | 
				
			|||
	//  - max one "addr-spec" in the "to" component
 | 
				
			|||
	Ok(value.to_string())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
// TODO: implement other URI shemes
 | 
				
			|||
// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
 | 
				
			|||
// https://en.wikipedia.org/wiki/List_of_URI_schemes
 | 
				
			|||
// Exemples:
 | 
				
			|||
//   - P1: tel, sms
 | 
				
			|||
//   - P2: geo, maps
 | 
				
			|||
//   - P3: irc, irc6, ircs, xmpp
 | 
				
			|||
//   - P4: sip, sips
 | 
				
			|||
#[derive(Clone, Debug, PartialEq)]
 | 
				
			|||
pub enum ContactType {
 | 
				
			|||
	Mailto,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ContactType {
 | 
				
			|||
	pub fn clean_value(&self, value: &str) -> Result<String, Error> {
 | 
				
			|||
		match self {
 | 
				
			|||
			ContactType::Mailto => clean_mailto(value),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl FromStr for ContactType {
 | 
				
			|||
	type Err = Error;
 | 
				
			|||
 | 
				
			|||
	fn from_str(s: &str) -> Result<Self, Error> {
 | 
				
			|||
		match s.to_lowercase().as_str() {
 | 
				
			|||
			"mailto" => Ok(ContactType::Mailto),
 | 
				
			|||
			_ => Err(format!("{s}: unknown contact type.").into()),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for ContactType {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			ContactType::Mailto => "mailto",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, PartialEq)]
 | 
				
			|||
pub struct AccountContact {
 | 
				
			|||
	pub contact_type: ContactType,
 | 
				
			|||
	pub value: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountContact {
 | 
				
			|||
	pub fn new(contact_type: &str, value: &str) -> Result<Self, Error> {
 | 
				
			|||
		let contact_type: ContactType = contact_type.parse()?;
 | 
				
			|||
		let value = contact_type.clean_value(value)?;
 | 
				
			|||
		Ok(AccountContact {
 | 
				
			|||
			contact_type,
 | 
				
			|||
			value,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for AccountContact {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		write!(f, "{}:{}", self.contact_type, self.value)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::*;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_contact_eq() {
 | 
				
			|||
		let c1 = AccountContact::new("mailto", "derp.derpson@example.com").unwrap();
 | 
				
			|||
		let c2 = AccountContact::new("mailto", "derp.derpson@example.com").unwrap();
 | 
				
			|||
		let c3 = AccountContact::new("mailto", "derp@example.com").unwrap();
 | 
				
			|||
		assert_eq!(c1, c2);
 | 
				
			|||
		assert_eq!(c2, c1);
 | 
				
			|||
		assert_ne!(c1, c3);
 | 
				
			|||
		assert_ne!(c2, c3);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_contact_in_vec() {
 | 
				
			|||
		let contacts = vec![
 | 
				
			|||
			AccountContact::new("mailto", "derp.derpson@example.com").unwrap(),
 | 
				
			|||
			AccountContact::new("mailto", "derp@example.com").unwrap(),
 | 
				
			|||
		];
 | 
				
			|||
		let c = AccountContact::new("mailto", "derp@example.com").unwrap();
 | 
				
			|||
		assert!(contacts.contains(&c));
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_contact_not_in_vec() {
 | 
				
			|||
		let contacts = vec![
 | 
				
			|||
			AccountContact::new("mailto", "derp.derpson@example.com").unwrap(),
 | 
				
			|||
			AccountContact::new("mailto", "derp@example.com").unwrap(),
 | 
				
			|||
		];
 | 
				
			|||
		let c = AccountContact::new("mailto", "derpina@example.com").unwrap();
 | 
				
			|||
		assert!(!contacts.contains(&c));
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,186 +0,0 @@ | 
				
			|||
use crate::account::contact::AccountContact;
 | 
				
			|||
use crate::account::{Account, AccountEndpoint, AccountKey, ExternalAccount};
 | 
				
			|||
use crate::storage::{account_files_exists, get_account_data, set_account_data, FileManager};
 | 
				
			|||
use acme_common::crypto::KeyPair;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::{Deserialize, Serialize};
 | 
				
			|||
use std::collections::HashMap;
 | 
				
			|||
use std::time::SystemTime;
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
 | 
				
			|||
pub struct ExternalAccountStorage {
 | 
				
			|||
	pub identifier: String,
 | 
				
			|||
	pub key: Vec<u8>,
 | 
				
			|||
	pub signature_algorithm: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ExternalAccountStorage {
 | 
				
			|||
	fn new(external_account: &ExternalAccount) -> Self {
 | 
				
			|||
		ExternalAccountStorage {
 | 
				
			|||
			identifier: external_account.identifier.to_owned(),
 | 
				
			|||
			key: external_account.key.to_owned(),
 | 
				
			|||
			signature_algorithm: external_account.signature_algorithm.to_string(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn to_generic(&self) -> Result<ExternalAccount, Error> {
 | 
				
			|||
		Ok(ExternalAccount {
 | 
				
			|||
			identifier: self.identifier.to_owned(),
 | 
				
			|||
			key: self.key.to_owned(),
 | 
				
			|||
			signature_algorithm: self.signature_algorithm.parse()?,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
 | 
				
			|||
struct AccountKeyStorage {
 | 
				
			|||
	creation_date: SystemTime,
 | 
				
			|||
	key: Vec<u8>,
 | 
				
			|||
	signature_algorithm: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountKeyStorage {
 | 
				
			|||
	fn new(key: &AccountKey) -> Result<Self, Error> {
 | 
				
			|||
		Ok(AccountKeyStorage {
 | 
				
			|||
			creation_date: key.creation_date,
 | 
				
			|||
			key: key.key.private_key_to_der()?,
 | 
				
			|||
			signature_algorithm: key.signature_algorithm.to_string(),
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn to_generic(&self) -> Result<AccountKey, Error> {
 | 
				
			|||
		Ok(AccountKey {
 | 
				
			|||
			creation_date: self.creation_date,
 | 
				
			|||
			key: KeyPair::from_der(&self.key)?,
 | 
				
			|||
			signature_algorithm: self.signature_algorithm.parse()?,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
 | 
				
			|||
struct AccountEndpointStorage {
 | 
				
			|||
	creation_date: SystemTime,
 | 
				
			|||
	account_url: String,
 | 
				
			|||
	orders_url: String,
 | 
				
			|||
	key_hash: Vec<u8>,
 | 
				
			|||
	contacts_hash: Vec<u8>,
 | 
				
			|||
	external_account_hash: Vec<u8>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountEndpointStorage {
 | 
				
			|||
	fn new(account_endpoint: &AccountEndpoint) -> Self {
 | 
				
			|||
		AccountEndpointStorage {
 | 
				
			|||
			creation_date: account_endpoint.creation_date,
 | 
				
			|||
			account_url: account_endpoint.account_url.clone(),
 | 
				
			|||
			orders_url: account_endpoint.orders_url.clone(),
 | 
				
			|||
			key_hash: account_endpoint.key_hash.clone(),
 | 
				
			|||
			contacts_hash: account_endpoint.contacts_hash.clone(),
 | 
				
			|||
			external_account_hash: account_endpoint.external_account_hash.clone(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn to_generic(&self) -> AccountEndpoint {
 | 
				
			|||
		AccountEndpoint {
 | 
				
			|||
			creation_date: self.creation_date,
 | 
				
			|||
			account_url: self.account_url.clone(),
 | 
				
			|||
			orders_url: self.orders_url.clone(),
 | 
				
			|||
			key_hash: self.key_hash.clone(),
 | 
				
			|||
			contacts_hash: self.contacts_hash.clone(),
 | 
				
			|||
			external_account_hash: self.external_account_hash.clone(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
 | 
				
			|||
struct AccountStorage {
 | 
				
			|||
	name: String,
 | 
				
			|||
	endpoints: HashMap<String, AccountEndpointStorage>,
 | 
				
			|||
	contacts: Vec<(String, String)>,
 | 
				
			|||
	current_key: AccountKeyStorage,
 | 
				
			|||
	past_keys: Vec<AccountKeyStorage>,
 | 
				
			|||
	external_account: Option<ExternalAccountStorage>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn do_fetch(file_manager: &FileManager, name: &str) -> Result<Option<Account>, Error> {
 | 
				
			|||
	if account_files_exists(file_manager) {
 | 
				
			|||
		let data = get_account_data(file_manager).await?;
 | 
				
			|||
		let obj: AccountStorage = bincode::deserialize(&data[..])
 | 
				
			|||
			.map_err(|e| Error::from(&e.to_string()).prefix(name))?;
 | 
				
			|||
		let endpoints = obj
 | 
				
			|||
			.endpoints
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|(k, v)| (k.clone(), v.to_generic()))
 | 
				
			|||
			.collect();
 | 
				
			|||
		let contacts = obj
 | 
				
			|||
			.contacts
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|(t, v)| AccountContact::new(t, v))
 | 
				
			|||
			.collect::<Result<Vec<AccountContact>, Error>>()?;
 | 
				
			|||
		let current_key = obj.current_key.to_generic()?;
 | 
				
			|||
		let past_keys = obj
 | 
				
			|||
			.past_keys
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|k| k.to_generic())
 | 
				
			|||
			.collect::<Result<Vec<AccountKey>, Error>>()?;
 | 
				
			|||
		let external_account = match obj.external_account {
 | 
				
			|||
			Some(a) => Some(a.to_generic()?),
 | 
				
			|||
			None => None,
 | 
				
			|||
		};
 | 
				
			|||
		Ok(Some(Account {
 | 
				
			|||
			name: obj.name,
 | 
				
			|||
			endpoints,
 | 
				
			|||
			contacts,
 | 
				
			|||
			current_key,
 | 
				
			|||
			past_keys,
 | 
				
			|||
			file_manager: file_manager.clone(),
 | 
				
			|||
			external_account,
 | 
				
			|||
		}))
 | 
				
			|||
	} else {
 | 
				
			|||
		Ok(None)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn do_save(file_manager: &FileManager, account: &Account) -> Result<(), Error> {
 | 
				
			|||
	let endpoints: HashMap<String, AccountEndpointStorage> = account
 | 
				
			|||
		.endpoints
 | 
				
			|||
		.iter()
 | 
				
			|||
		.map(|(k, v)| (k.to_owned(), AccountEndpointStorage::new(v)))
 | 
				
			|||
		.collect();
 | 
				
			|||
	let contacts: Vec<(String, String)> = account
 | 
				
			|||
		.contacts
 | 
				
			|||
		.iter()
 | 
				
			|||
		.map(|c| (c.contact_type.to_string(), c.value.to_owned()))
 | 
				
			|||
		.collect();
 | 
				
			|||
	let past_keys = account
 | 
				
			|||
		.past_keys
 | 
				
			|||
		.iter()
 | 
				
			|||
		.map(AccountKeyStorage::new)
 | 
				
			|||
		.collect::<Result<Vec<AccountKeyStorage>, Error>>()?;
 | 
				
			|||
	let external_account = account
 | 
				
			|||
		.external_account
 | 
				
			|||
		.as_ref()
 | 
				
			|||
		.map(ExternalAccountStorage::new);
 | 
				
			|||
	let account_storage = AccountStorage {
 | 
				
			|||
		name: account.name.to_owned(),
 | 
				
			|||
		endpoints,
 | 
				
			|||
		contacts,
 | 
				
			|||
		current_key: AccountKeyStorage::new(&account.current_key)?,
 | 
				
			|||
		past_keys,
 | 
				
			|||
		external_account,
 | 
				
			|||
	};
 | 
				
			|||
	let encoded: Vec<u8> = bincode::serialize(&account_storage)
 | 
				
			|||
		.map_err(|e| Error::from(&e.to_string()).prefix(&account.name))?;
 | 
				
			|||
	set_account_data(file_manager, &encoded).await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn fetch(file_manager: &FileManager, name: &str) -> Result<Option<Account>, Error> {
 | 
				
			|||
	do_fetch(file_manager, name).await.map_err(|_| {
 | 
				
			|||
		format!("account \"{name}\": unable to load account file: file may be corrupted").into()
 | 
				
			|||
	})
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn save(file_manager: &FileManager, account: &Account) -> Result<(), Error> {
 | 
				
			|||
	do_save(file_manager, account)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(|e| format!("unable to save account file: {e}").into())
 | 
				
			|||
}
 | 
				
			|||
@ -1,292 +0,0 @@ | 
				
			|||
use crate::acme_proto::structs::{
 | 
				
			|||
	AcmeError, ApiError, Authorization, AuthorizationStatus, NewOrder, Order, OrderStatus,
 | 
				
			|||
};
 | 
				
			|||
use crate::certificate::Certificate;
 | 
				
			|||
use crate::http::HttpError;
 | 
				
			|||
use crate::identifier::IdentifierType;
 | 
				
			|||
use crate::jws::encode_kid;
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::storage;
 | 
				
			|||
use crate::{AccountSync, EndpointSync};
 | 
				
			|||
use acme_common::crypto::Csr;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde_json::json;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
 | 
				
			|||
pub mod account;
 | 
				
			|||
mod certificate;
 | 
				
			|||
mod http;
 | 
				
			|||
pub mod structs;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Copy, Debug, PartialEq)]
 | 
				
			|||
pub enum Challenge {
 | 
				
			|||
	Http01,
 | 
				
			|||
	Dns01,
 | 
				
			|||
	TlsAlpn01,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Challenge {
 | 
				
			|||
	pub fn from_str(s: &str) -> Result<Self, Error> {
 | 
				
			|||
		match s.to_lowercase().as_str() {
 | 
				
			|||
			"http-01" => Ok(Challenge::Http01),
 | 
				
			|||
			"dns-01" => Ok(Challenge::Dns01),
 | 
				
			|||
			"tls-alpn-01" => Ok(Challenge::TlsAlpn01),
 | 
				
			|||
			_ => Err(format!("{s}: unknown challenge.").into()),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Challenge {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			Challenge::Http01 => "http-01",
 | 
				
			|||
			Challenge::Dns01 => "dns-01",
 | 
				
			|||
			Challenge::TlsAlpn01 => "tls-alpn-01",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl PartialEq<structs::Challenge> for Challenge {
 | 
				
			|||
	fn eq(&self, other: &structs::Challenge) -> bool {
 | 
				
			|||
		matches!(
 | 
				
			|||
			(self, other),
 | 
				
			|||
			(Challenge::Http01, structs::Challenge::Http01(_))
 | 
				
			|||
				| (Challenge::Dns01, structs::Challenge::Dns01(_))
 | 
				
			|||
				| (Challenge::TlsAlpn01, structs::Challenge::TlsAlpn01(_))
 | 
				
			|||
		)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[macro_export]
 | 
				
			|||
macro_rules! set_data_builder_sync {
 | 
				
			|||
	($account: ident, $endpoint_name: ident, $data: expr) => {{
 | 
				
			|||
		let endpoint_name = &$endpoint_name;
 | 
				
			|||
		move |n: &str, url: &str| {
 | 
				
			|||
			encode_kid(
 | 
				
			|||
				&$account.current_key.key,
 | 
				
			|||
				&$account.current_key.signature_algorithm,
 | 
				
			|||
				&($account.get_endpoint(endpoint_name)?.account_url),
 | 
				
			|||
				$data,
 | 
				
			|||
				url,
 | 
				
			|||
				n,
 | 
				
			|||
			)
 | 
				
			|||
		}
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[macro_export]
 | 
				
			|||
macro_rules! set_data_builder {
 | 
				
			|||
	($account: ident, $endpoint_name: ident, $data: expr) => {
 | 
				
			|||
		async {
 | 
				
			|||
			let account = $account.read().await;
 | 
				
			|||
			set_data_builder_sync!(account, $endpoint_name, $data)
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn request_certificate(
 | 
				
			|||
	cert: &Certificate,
 | 
				
			|||
	account_s: AccountSync,
 | 
				
			|||
	endpoint_s: EndpointSync,
 | 
				
			|||
) -> Result<(), Error> {
 | 
				
			|||
	let mut hook_datas = vec![];
 | 
				
			|||
	let endpoint_name = endpoint_s.read().await.name.clone();
 | 
				
			|||
 | 
				
			|||
	// Refresh the directory
 | 
				
			|||
	http::refresh_directory(&mut *(endpoint_s.write().await))
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(HttpError::in_err)?;
 | 
				
			|||
 | 
				
			|||
	// Synchronize the account
 | 
				
			|||
	account_s
 | 
				
			|||
		.write()
 | 
				
			|||
		.await
 | 
				
			|||
		.synchronize(&mut *(endpoint_s.write().await))
 | 
				
			|||
		.await?;
 | 
				
			|||
 | 
				
			|||
	// Create a new order
 | 
				
			|||
	let mut new_reg = false;
 | 
				
			|||
	let (order, order_url) = loop {
 | 
				
			|||
		let new_order = NewOrder::new(&cert.identifiers);
 | 
				
			|||
		let new_order = serde_json::to_string(&new_order)?;
 | 
				
			|||
		let data_builder = set_data_builder!(account_s, endpoint_name, new_order.as_bytes()).await;
 | 
				
			|||
		match http::new_order(&mut *(endpoint_s.write().await), &data_builder).await {
 | 
				
			|||
			Ok((order, order_url)) => {
 | 
				
			|||
				if let Some(e) = order.get_error() {
 | 
				
			|||
					cert.warn(&e.prefix("Error").message);
 | 
				
			|||
				}
 | 
				
			|||
				break (order, order_url);
 | 
				
			|||
			}
 | 
				
			|||
			Err(e) => {
 | 
				
			|||
				if !new_reg && e.is_acme_err(AcmeError::AccountDoesNotExist) {
 | 
				
			|||
					drop(data_builder);
 | 
				
			|||
					account_s
 | 
				
			|||
						.write()
 | 
				
			|||
						.await
 | 
				
			|||
						.register(&mut *(endpoint_s.write().await))
 | 
				
			|||
						.await?;
 | 
				
			|||
					new_reg = true;
 | 
				
			|||
				} else {
 | 
				
			|||
					return Err(HttpError::in_err(e));
 | 
				
			|||
				}
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
	};
 | 
				
			|||
 | 
				
			|||
	// Begin iter over authorizations
 | 
				
			|||
	for auth_url in order.authorizations.iter() {
 | 
				
			|||
		// Fetch the authorization
 | 
				
			|||
		let data_builder = set_data_builder!(account_s, endpoint_name, b"").await;
 | 
				
			|||
		let auth =
 | 
				
			|||
			http::get_authorization(&mut *(endpoint_s.write().await), &data_builder, auth_url)
 | 
				
			|||
				.await
 | 
				
			|||
				.map_err(HttpError::in_err)?;
 | 
				
			|||
		drop(data_builder);
 | 
				
			|||
		if let Some(e) = auth.get_error() {
 | 
				
			|||
			cert.warn(&e.prefix("error").message);
 | 
				
			|||
		}
 | 
				
			|||
		if auth.status == AuthorizationStatus::Valid {
 | 
				
			|||
			continue;
 | 
				
			|||
		}
 | 
				
			|||
		if auth.status != AuthorizationStatus::Pending {
 | 
				
			|||
			let msg = format!(
 | 
				
			|||
				"{}: authorization status is {}",
 | 
				
			|||
				auth.identifier, auth.status
 | 
				
			|||
			);
 | 
				
			|||
			return Err(msg.into());
 | 
				
			|||
		}
 | 
				
			|||
 | 
				
			|||
		// Fetch the associated challenges
 | 
				
			|||
		let current_identifier = cert.get_identifier_from_str(&auth.identifier.value)?;
 | 
				
			|||
		let current_challenge = current_identifier.challenge;
 | 
				
			|||
		for challenge in auth.challenges.iter() {
 | 
				
			|||
			if current_challenge == *challenge {
 | 
				
			|||
				let (proof, raw_proof) =
 | 
				
			|||
					challenge.get_proof(&account_s.read().await.current_key.key)?;
 | 
				
			|||
				let file_name = challenge.get_file_name();
 | 
				
			|||
				let identifier = auth.identifier.value.to_owned();
 | 
				
			|||
 | 
				
			|||
				// Call the challenge hook in order to complete it
 | 
				
			|||
				let mut data = cert
 | 
				
			|||
					.call_challenge_hooks(&file_name, &proof, raw_proof, &identifier)
 | 
				
			|||
					.await?;
 | 
				
			|||
				data.0.is_clean_hook = true;
 | 
				
			|||
				hook_datas.push(data);
 | 
				
			|||
 | 
				
			|||
				// Tell the server the challenge has been completed
 | 
				
			|||
				let chall_url = challenge.get_url();
 | 
				
			|||
				let data_builder = set_data_builder!(account_s, endpoint_name, b"{}").await;
 | 
				
			|||
				http::post_jose_no_response(
 | 
				
			|||
					&mut *(endpoint_s.write().await),
 | 
				
			|||
					&data_builder,
 | 
				
			|||
					&chall_url,
 | 
				
			|||
				)
 | 
				
			|||
				.await
 | 
				
			|||
				.map_err(HttpError::in_err)?;
 | 
				
			|||
				drop(data_builder);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
 | 
				
			|||
		// Pool the authorization in order to see whether or not it is valid
 | 
				
			|||
		let data_builder = set_data_builder!(account_s, endpoint_name, b"").await;
 | 
				
			|||
		let break_fn = |a: &Authorization| a.status == AuthorizationStatus::Valid;
 | 
				
			|||
		let _ = http::pool_authorization(
 | 
				
			|||
			&mut *(endpoint_s.write().await),
 | 
				
			|||
			&data_builder,
 | 
				
			|||
			&break_fn,
 | 
				
			|||
			auth_url,
 | 
				
			|||
		)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(HttpError::in_err)?;
 | 
				
			|||
		drop(data_builder);
 | 
				
			|||
		for (data, hook_type) in hook_datas.iter() {
 | 
				
			|||
			cert.call_challenge_hooks_clean(data, (*hook_type).to_owned())
 | 
				
			|||
				.await?;
 | 
				
			|||
		}
 | 
				
			|||
		hook_datas.clear();
 | 
				
			|||
	}
 | 
				
			|||
	// End iter over authorizations
 | 
				
			|||
 | 
				
			|||
	// Pool the order in order to see whether or not it is ready
 | 
				
			|||
	let data_builder = set_data_builder!(account_s, endpoint_name, b"").await;
 | 
				
			|||
	let break_fn = |o: &Order| o.status == OrderStatus::Ready;
 | 
				
			|||
	let order = http::pool_order(
 | 
				
			|||
		&mut *(endpoint_s.write().await),
 | 
				
			|||
		&data_builder,
 | 
				
			|||
		&break_fn,
 | 
				
			|||
		&order_url,
 | 
				
			|||
	)
 | 
				
			|||
	.await
 | 
				
			|||
	.map_err(HttpError::in_err)?;
 | 
				
			|||
	drop(data_builder);
 | 
				
			|||
 | 
				
			|||
	// Finalize the order by sending the CSR
 | 
				
			|||
	let key_pair = certificate::get_key_pair(cert).await?;
 | 
				
			|||
	let domains: Vec<String> = cert
 | 
				
			|||
		.identifiers
 | 
				
			|||
		.iter()
 | 
				
			|||
		.filter(|e| e.id_type == IdentifierType::Dns)
 | 
				
			|||
		.map(|e| e.value.to_owned())
 | 
				
			|||
		.collect();
 | 
				
			|||
	let ips: Vec<String> = cert
 | 
				
			|||
		.identifiers
 | 
				
			|||
		.iter()
 | 
				
			|||
		.filter(|e| e.id_type == IdentifierType::Ip)
 | 
				
			|||
		.map(|e| e.value.to_owned())
 | 
				
			|||
		.collect();
 | 
				
			|||
	let csr = Csr::new(
 | 
				
			|||
		&key_pair,
 | 
				
			|||
		cert.csr_digest,
 | 
				
			|||
		domains.as_slice(),
 | 
				
			|||
		ips.as_slice(),
 | 
				
			|||
		&cert.subject_attributes,
 | 
				
			|||
	)?;
 | 
				
			|||
	cert.trace(&format!("new CSR:\n{}", csr.to_pem()?));
 | 
				
			|||
	let csr = json!({
 | 
				
			|||
		"csr": csr.to_der_base64()?,
 | 
				
			|||
	});
 | 
				
			|||
	let csr = csr.to_string();
 | 
				
			|||
	let data_builder = set_data_builder!(account_s, endpoint_name, csr.as_bytes()).await;
 | 
				
			|||
	let order = http::finalize_order(
 | 
				
			|||
		&mut *(endpoint_s.write().await),
 | 
				
			|||
		&data_builder,
 | 
				
			|||
		&order.finalize,
 | 
				
			|||
	)
 | 
				
			|||
	.await
 | 
				
			|||
	.map_err(HttpError::in_err)?;
 | 
				
			|||
	drop(data_builder);
 | 
				
			|||
	if let Some(e) = order.get_error() {
 | 
				
			|||
		cert.warn(&e.prefix("error").message);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	// Pool the order in order to see whether or not it is valid
 | 
				
			|||
	let data_builder = set_data_builder!(account_s, endpoint_name, b"").await;
 | 
				
			|||
	let break_fn = |o: &Order| o.status == OrderStatus::Valid;
 | 
				
			|||
	let order = http::pool_order(
 | 
				
			|||
		&mut *(endpoint_s.write().await),
 | 
				
			|||
		&data_builder,
 | 
				
			|||
		&break_fn,
 | 
				
			|||
		&order_url,
 | 
				
			|||
	)
 | 
				
			|||
	.await
 | 
				
			|||
	.map_err(HttpError::in_err)?;
 | 
				
			|||
	drop(data_builder);
 | 
				
			|||
 | 
				
			|||
	// Download the certificate
 | 
				
			|||
	let crt_url = order
 | 
				
			|||
		.certificate
 | 
				
			|||
		.ok_or_else(|| Error::from("no certificate available for download"))?;
 | 
				
			|||
	let data_builder = set_data_builder!(account_s, endpoint_name, b"").await;
 | 
				
			|||
	let crt = http::get_certificate(&mut *(endpoint_s.write().await), &data_builder, &crt_url)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(HttpError::in_err)?;
 | 
				
			|||
	drop(data_builder);
 | 
				
			|||
	storage::write_certificate(&cert.file_manager, crt.as_bytes()).await?;
 | 
				
			|||
 | 
				
			|||
	cert.info(&format!(
 | 
				
			|||
		"certificate renewed (identifiers: {})",
 | 
				
			|||
		cert.identifier_list()
 | 
				
			|||
	));
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
@ -1,154 +0,0 @@ | 
				
			|||
use crate::account::Account as BaseAccount;
 | 
				
			|||
use crate::acme_proto::http;
 | 
				
			|||
use crate::acme_proto::structs::{Account, AccountKeyRollover, AccountUpdate, AcmeError};
 | 
				
			|||
use crate::endpoint::Endpoint;
 | 
				
			|||
use crate::http::HttpError;
 | 
				
			|||
use crate::jws::{encode_jwk, encode_kid};
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::set_data_builder_sync;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
 | 
				
			|||
macro_rules! create_account_if_does_not_exist {
 | 
				
			|||
	($e: expr, $endpoint: ident, $account: ident) => {
 | 
				
			|||
		match $e {
 | 
				
			|||
			Ok(r) => Ok(r),
 | 
				
			|||
			Err(he) => match he {
 | 
				
			|||
				HttpError::ApiError(ref e) => match e.get_acme_type() {
 | 
				
			|||
					AcmeError::AccountDoesNotExist => {
 | 
				
			|||
						let msg = format!(
 | 
				
			|||
							"account has been dropped by endpoint \"{}\"",
 | 
				
			|||
							$endpoint.name
 | 
				
			|||
						);
 | 
				
			|||
						$account.debug(&msg);
 | 
				
			|||
						return register_account($endpoint, $account).await;
 | 
				
			|||
					}
 | 
				
			|||
					_ => Err(HttpError::in_err(he.to_owned())),
 | 
				
			|||
				},
 | 
				
			|||
				HttpError::GenericError(e) => Err(e),
 | 
				
			|||
			},
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn register_account(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	account: &mut BaseAccount,
 | 
				
			|||
) -> Result<(), Error> {
 | 
				
			|||
	account.debug(&format!(
 | 
				
			|||
		"creating account on endpoint \"{}\"...",
 | 
				
			|||
		&endpoint.name
 | 
				
			|||
	));
 | 
				
			|||
	let account_struct = Account::new(account, endpoint)?;
 | 
				
			|||
	let account_struct = serde_json::to_string(&account_struct)?;
 | 
				
			|||
	let acc_ref = &account_struct;
 | 
				
			|||
	let kp_ref = &account.current_key.key;
 | 
				
			|||
	let signature_algorithm = &account.current_key.signature_algorithm;
 | 
				
			|||
	let data_builder = |n: &str, url: &str| {
 | 
				
			|||
		encode_jwk(
 | 
				
			|||
			kp_ref,
 | 
				
			|||
			signature_algorithm,
 | 
				
			|||
			acc_ref.as_bytes(),
 | 
				
			|||
			url,
 | 
				
			|||
			Some(n.to_string()),
 | 
				
			|||
		)
 | 
				
			|||
	};
 | 
				
			|||
	let (acc_rep, account_url) = http::new_account(endpoint, &data_builder)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(HttpError::in_err)?;
 | 
				
			|||
	account.set_account_url(&endpoint.name, &account_url)?;
 | 
				
			|||
	let orders_url = match acc_rep.orders {
 | 
				
			|||
		Some(url) => url,
 | 
				
			|||
		None => {
 | 
				
			|||
			let msg = format!(
 | 
				
			|||
				"endpoint \"{}\": account \"{}\": the server has not provided an order URL upon account creation",
 | 
				
			|||
				&endpoint.name,
 | 
				
			|||
				&account.name
 | 
				
			|||
			);
 | 
				
			|||
			account.warn(&msg);
 | 
				
			|||
			String::new()
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	account.set_orders_url(&endpoint.name, &orders_url)?;
 | 
				
			|||
	account.update_key_hash(&endpoint.name)?;
 | 
				
			|||
	account.update_contacts_hash(&endpoint.name)?;
 | 
				
			|||
	account.update_external_account_hash(&endpoint.name)?;
 | 
				
			|||
	account.save().await?;
 | 
				
			|||
	account.info(&format!(
 | 
				
			|||
		"account created on endpoint \"{}\"",
 | 
				
			|||
		&endpoint.name
 | 
				
			|||
	));
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn update_account_contacts(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	account: &mut BaseAccount,
 | 
				
			|||
) -> Result<(), Error> {
 | 
				
			|||
	let endpoint_name = endpoint.name.clone();
 | 
				
			|||
	account.debug(&format!(
 | 
				
			|||
		"updating account contacts on endpoint \"{endpoint_name}\"..."
 | 
				
			|||
	));
 | 
				
			|||
	let new_contacts: Vec<String> = account.contacts.iter().map(|c| c.to_string()).collect();
 | 
				
			|||
	let acc_up_struct = AccountUpdate::new(&new_contacts);
 | 
				
			|||
	let acc_up_struct = serde_json::to_string(&acc_up_struct)?;
 | 
				
			|||
	let account_owned = account.clone();
 | 
				
			|||
	let data_builder =
 | 
				
			|||
		set_data_builder_sync!(account_owned, endpoint_name, acc_up_struct.as_bytes());
 | 
				
			|||
	let url = account.get_endpoint(&endpoint_name)?.account_url.clone();
 | 
				
			|||
	create_account_if_does_not_exist!(
 | 
				
			|||
		http::post_jose_no_response(endpoint, &data_builder, &url).await,
 | 
				
			|||
		endpoint,
 | 
				
			|||
		account
 | 
				
			|||
	)?;
 | 
				
			|||
	account.update_contacts_hash(&endpoint_name)?;
 | 
				
			|||
	account.save().await?;
 | 
				
			|||
	account.info(&format!(
 | 
				
			|||
		"account contacts updated on endpoint \"{endpoint_name}\""
 | 
				
			|||
	));
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn update_account_key(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	account: &mut BaseAccount,
 | 
				
			|||
) -> Result<(), Error> {
 | 
				
			|||
	let endpoint_name = endpoint.name.clone();
 | 
				
			|||
	account.debug(&format!(
 | 
				
			|||
		"updating account key on endpoint \"{endpoint_name}\"..."
 | 
				
			|||
	));
 | 
				
			|||
	let url = endpoint.dir.key_change.clone();
 | 
				
			|||
	let ep = account.get_endpoint(&endpoint_name)?;
 | 
				
			|||
	let old_account_key = account.get_past_key(&ep.key_hash)?;
 | 
				
			|||
	let old_key = &old_account_key.key;
 | 
				
			|||
	let account_url = account.get_endpoint(&endpoint_name)?.account_url.clone();
 | 
				
			|||
	let rollover_struct = AccountKeyRollover::new(&account_url, old_key)?;
 | 
				
			|||
	let rollover_struct = serde_json::to_string(&rollover_struct)?;
 | 
				
			|||
	let rollover_payload = encode_jwk(
 | 
				
			|||
		&account.current_key.key,
 | 
				
			|||
		&account.current_key.signature_algorithm,
 | 
				
			|||
		rollover_struct.as_bytes(),
 | 
				
			|||
		&url,
 | 
				
			|||
		None,
 | 
				
			|||
	)?;
 | 
				
			|||
	let data_builder = |n: &str, url: &str| {
 | 
				
			|||
		encode_kid(
 | 
				
			|||
			old_key,
 | 
				
			|||
			&old_account_key.signature_algorithm,
 | 
				
			|||
			&account_url,
 | 
				
			|||
			rollover_payload.as_bytes(),
 | 
				
			|||
			url,
 | 
				
			|||
			n,
 | 
				
			|||
		)
 | 
				
			|||
	};
 | 
				
			|||
	create_account_if_does_not_exist!(
 | 
				
			|||
		http::post_jose_no_response(endpoint, &data_builder, &url).await,
 | 
				
			|||
		endpoint,
 | 
				
			|||
		account
 | 
				
			|||
	)?;
 | 
				
			|||
	account.update_key_hash(&endpoint_name)?;
 | 
				
			|||
	account.save().await?;
 | 
				
			|||
	account.info(&format!(
 | 
				
			|||
		"account key updated on endpoint \"{endpoint_name}\""
 | 
				
			|||
	));
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
@ -1,25 +0,0 @@ | 
				
			|||
use crate::certificate::Certificate;
 | 
				
			|||
use crate::storage;
 | 
				
			|||
use acme_common::crypto::{gen_keypair, KeyPair};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
 | 
				
			|||
async fn gen_key_pair(cert: &Certificate) -> Result<KeyPair, Error> {
 | 
				
			|||
	let key_pair = gen_keypair(cert.key_type)?;
 | 
				
			|||
	storage::set_keypair(&cert.file_manager, &key_pair).await?;
 | 
				
			|||
	Ok(key_pair)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn read_key_pair(cert: &Certificate) -> Result<KeyPair, Error> {
 | 
				
			|||
	storage::get_keypair(&cert.file_manager).await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_key_pair(cert: &Certificate) -> Result<KeyPair, Error> {
 | 
				
			|||
	if cert.kp_reuse {
 | 
				
			|||
		match read_key_pair(cert).await {
 | 
				
			|||
			Ok(key_pair) => Ok(key_pair),
 | 
				
			|||
			Err(_) => gen_key_pair(cert).await,
 | 
				
			|||
		}
 | 
				
			|||
	} else {
 | 
				
			|||
		gen_key_pair(cert).await
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,149 +0,0 @@ | 
				
			|||
use crate::acme_proto::structs::{AccountResponse, Authorization, Directory, Order};
 | 
				
			|||
use crate::endpoint::Endpoint;
 | 
				
			|||
use crate::http;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use std::{thread, time};
 | 
				
			|||
 | 
				
			|||
macro_rules! pool_object {
 | 
				
			|||
	($obj_type: ty, $obj_name: expr, $endpoint: expr, $url: expr, $data_builder: expr, $break: expr) => {{
 | 
				
			|||
		for _ in 0..crate::DEFAULT_POOL_NB_TRIES {
 | 
				
			|||
			thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC));
 | 
				
			|||
			let response = http::post_jose($endpoint, $url, $data_builder).await?;
 | 
				
			|||
			let obj = response.json::<$obj_type>()?;
 | 
				
			|||
			if $break(&obj) {
 | 
				
			|||
				return Ok(obj);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		let msg = format!("{} pooling failed on {}", $obj_name, $url);
 | 
				
			|||
		Err(msg.into())
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn refresh_directory(endpoint: &mut Endpoint) -> Result<(), http::HttpError> {
 | 
				
			|||
	let url = endpoint.url.clone();
 | 
				
			|||
	let response = http::get(endpoint, &url).await?;
 | 
				
			|||
	endpoint.dir = response.json::<Directory>()?;
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn post_jose_no_response<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<(), http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let _ = http::post_jose(endpoint, url, data_builder).await?;
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn new_account<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
) -> Result<(AccountResponse, String), http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let url = endpoint.dir.new_account.clone();
 | 
				
			|||
	let response = http::post_jose(endpoint, &url, data_builder).await?;
 | 
				
			|||
	let acc_uri = response
 | 
				
			|||
		.get_header(http::HEADER_LOCATION)
 | 
				
			|||
		.ok_or_else(|| Error::from("no account location found"))?;
 | 
				
			|||
	let acc_resp = response.json::<AccountResponse>()?;
 | 
				
			|||
	Ok((acc_resp, acc_uri))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn new_order<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
) -> Result<(Order, String), http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let url = endpoint.dir.new_order.clone();
 | 
				
			|||
	let response = http::post_jose(endpoint, &url, data_builder).await?;
 | 
				
			|||
	let order_uri = response
 | 
				
			|||
		.get_header(http::HEADER_LOCATION)
 | 
				
			|||
		.ok_or_else(|| Error::from("no account location found"))?;
 | 
				
			|||
	let order_resp = response.json::<Order>()?;
 | 
				
			|||
	Ok((order_resp, order_uri))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_authorization<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<Authorization, http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let response = http::post_jose(endpoint, url, data_builder).await?;
 | 
				
			|||
	let auth = response.json::<Authorization>()?;
 | 
				
			|||
	Ok(auth)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn pool_authorization<F, S>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	break_fn: &S,
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<Authorization, http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
	S: Fn(&Authorization) -> bool,
 | 
				
			|||
{
 | 
				
			|||
	pool_object!(
 | 
				
			|||
		Authorization,
 | 
				
			|||
		"authorization",
 | 
				
			|||
		endpoint,
 | 
				
			|||
		url,
 | 
				
			|||
		data_builder,
 | 
				
			|||
		break_fn
 | 
				
			|||
	)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn pool_order<F, S>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	break_fn: &S,
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<Order, http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
	S: Fn(&Order) -> bool,
 | 
				
			|||
{
 | 
				
			|||
	pool_object!(Order, "order", endpoint, url, data_builder, break_fn)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn finalize_order<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<Order, http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let response = http::post_jose(endpoint, url, data_builder).await?;
 | 
				
			|||
	let order = response.json::<Order>()?;
 | 
				
			|||
	Ok(order)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_certificate<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<String, http::HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let response = http::post(
 | 
				
			|||
		endpoint,
 | 
				
			|||
		url,
 | 
				
			|||
		data_builder,
 | 
				
			|||
		http::CONTENT_TYPE_JOSE,
 | 
				
			|||
		http::CONTENT_TYPE_PEM,
 | 
				
			|||
	)
 | 
				
			|||
	.await?;
 | 
				
			|||
	Ok(response.body)
 | 
				
			|||
}
 | 
				
			|||
@ -1,29 +0,0 @@ | 
				
			|||
#[macro_export]
 | 
				
			|||
macro_rules! deserialize_from_str {
 | 
				
			|||
	($t: ty) => {
 | 
				
			|||
		impl FromStr for $t {
 | 
				
			|||
			type Err = Error;
 | 
				
			|||
 | 
				
			|||
			fn from_str(data: &str) -> Result<Self, Self::Err> {
 | 
				
			|||
				let res = serde_json::from_str(data)?;
 | 
				
			|||
				Ok(res)
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
mod account;
 | 
				
			|||
mod authorization;
 | 
				
			|||
mod directory;
 | 
				
			|||
mod error;
 | 
				
			|||
mod order;
 | 
				
			|||
 | 
				
			|||
#[allow(unused_imports)]
 | 
				
			|||
pub use account::{
 | 
				
			|||
	Account, AccountDeactivation, AccountKeyRollover, AccountResponse, AccountUpdate,
 | 
				
			|||
};
 | 
				
			|||
pub use authorization::{Authorization, AuthorizationStatus, Challenge};
 | 
				
			|||
pub use deserialize_from_str;
 | 
				
			|||
pub use directory::Directory;
 | 
				
			|||
pub use error::{AcmeError, ApiError, HttpApiError};
 | 
				
			|||
pub use order::{Identifier, NewOrder, Order, OrderStatus};
 | 
				
			|||
@ -1,202 +0,0 @@ | 
				
			|||
use crate::endpoint::Endpoint;
 | 
				
			|||
use crate::jws::encode_kid_mac;
 | 
				
			|||
use acme_common::crypto::KeyPair;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::{Deserialize, Serialize};
 | 
				
			|||
use serde_json::value::Value;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
pub struct Account {
 | 
				
			|||
	pub contact: Vec<String>,
 | 
				
			|||
	pub terms_of_service_agreed: bool,
 | 
				
			|||
	pub only_return_existing: bool,
 | 
				
			|||
	#[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			|||
	pub external_account_binding: Option<Value>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Account {
 | 
				
			|||
	pub fn new(account: &crate::account::Account, endpoint: &Endpoint) -> Result<Self, Error> {
 | 
				
			|||
		let external_account_binding = match &account.external_account {
 | 
				
			|||
			Some(a) => {
 | 
				
			|||
				let k_ref = &a.key;
 | 
				
			|||
				let signature_algorithm = &a.signature_algorithm;
 | 
				
			|||
				let kid = &a.identifier;
 | 
				
			|||
				let payload = account.current_key.key.jwk_public_key()?;
 | 
				
			|||
				let payload = serde_json::to_string(&payload)?;
 | 
				
			|||
				let data = encode_kid_mac(
 | 
				
			|||
					k_ref,
 | 
				
			|||
					signature_algorithm,
 | 
				
			|||
					kid,
 | 
				
			|||
					payload.as_bytes(),
 | 
				
			|||
					&endpoint.dir.new_account,
 | 
				
			|||
				)?;
 | 
				
			|||
				let data: Value = serde_json::from_str(&data)?;
 | 
				
			|||
				Some(data)
 | 
				
			|||
			}
 | 
				
			|||
			None => None,
 | 
				
			|||
		};
 | 
				
			|||
		Ok(Account {
 | 
				
			|||
			contact: account.contacts.iter().map(|e| e.to_string()).collect(),
 | 
				
			|||
			terms_of_service_agreed: endpoint.tos_agreed,
 | 
				
			|||
			only_return_existing: false,
 | 
				
			|||
			external_account_binding,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
pub struct AccountResponse {
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub status: String,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub contact: Option<Vec<String>>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub terms_of_service_agreed: Option<bool>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub external_account_binding: Option<Value>,
 | 
				
			|||
	pub orders: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
deserialize_from_str!(AccountResponse);
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
pub struct AccountUpdate {
 | 
				
			|||
	pub contact: Vec<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountUpdate {
 | 
				
			|||
	pub fn new(contact: &[String]) -> Self {
 | 
				
			|||
		AccountUpdate {
 | 
				
			|||
			contact: contact.into(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
pub struct AccountKeyRollover {
 | 
				
			|||
	pub account: String,
 | 
				
			|||
	pub old_key: Value,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountKeyRollover {
 | 
				
			|||
	pub fn new(account_str: &str, old_key: &KeyPair) -> Result<Self, Error> {
 | 
				
			|||
		Ok(AccountKeyRollover {
 | 
				
			|||
			account: account_str.to_string(),
 | 
				
			|||
			old_key: old_key.jwk_public_key()?,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
// TODO: implement account deactivation
 | 
				
			|||
#[allow(dead_code)]
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
pub struct AccountDeactivation {
 | 
				
			|||
	pub status: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountDeactivation {
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub fn new() -> Self {
 | 
				
			|||
		AccountDeactivation {
 | 
				
			|||
			status: "deactivated".into(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::*;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_new() {
 | 
				
			|||
		let emails = vec![
 | 
				
			|||
			"mailto:derp@example.com".to_string(),
 | 
				
			|||
			"mailto:derp.derpson@example.com".to_string(),
 | 
				
			|||
		];
 | 
				
			|||
		let a = Account {
 | 
				
			|||
			contact: emails,
 | 
				
			|||
			terms_of_service_agreed: true,
 | 
				
			|||
			only_return_existing: false,
 | 
				
			|||
			external_account_binding: None,
 | 
				
			|||
		};
 | 
				
			|||
		assert_eq!(a.contact.len(), 2);
 | 
				
			|||
		assert_eq!(a.terms_of_service_agreed, true);
 | 
				
			|||
		assert_eq!(a.only_return_existing, false);
 | 
				
			|||
		let a_str = serde_json::to_string(&a);
 | 
				
			|||
		assert!(a_str.is_ok());
 | 
				
			|||
		let a_str = a_str.unwrap();
 | 
				
			|||
		assert!(a_str.starts_with("{"));
 | 
				
			|||
		assert!(a_str.ends_with("}"));
 | 
				
			|||
		assert!(a_str.contains("\"contact\""));
 | 
				
			|||
		assert!(a_str.contains("\"mailto:derp@example.com\""));
 | 
				
			|||
		assert!(a_str.contains("\"mailto:derp.derpson@example.com\""));
 | 
				
			|||
		assert!(a_str.contains("\"termsOfServiceAgreed\""));
 | 
				
			|||
		assert!(a_str.contains("\"onlyReturnExisting\""));
 | 
				
			|||
		assert!(a_str.contains("true"));
 | 
				
			|||
		assert!(a_str.contains("false"));
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_response() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"status\": \"valid\",
 | 
				
			|||
	\"contact\": [
 | 
				
			|||
		\"mailto:cert-admin@example.org\",
 | 
				
			|||
		\"mailto:admin@example.org\"
 | 
				
			|||
	],
 | 
				
			|||
	\"termsOfServiceAgreed\": true,
 | 
				
			|||
	\"orders\": \"https://example.com/acme/orders/rzGoeA\"
 | 
				
			|||
}";
 | 
				
			|||
		let account_resp = AccountResponse::from_str(data);
 | 
				
			|||
		assert!(account_resp.is_ok());
 | 
				
			|||
		let account_resp = account_resp.unwrap();
 | 
				
			|||
		assert_eq!(account_resp.status, "valid");
 | 
				
			|||
		assert!(account_resp.contact.is_some());
 | 
				
			|||
		let contacts = account_resp.contact.unwrap();
 | 
				
			|||
		assert_eq!(contacts.len(), 2);
 | 
				
			|||
		assert_eq!(contacts[0], "mailto:cert-admin@example.org");
 | 
				
			|||
		assert_eq!(contacts[1], "mailto:admin@example.org");
 | 
				
			|||
		assert!(account_resp.external_account_binding.is_none());
 | 
				
			|||
		assert!(account_resp.terms_of_service_agreed.is_some());
 | 
				
			|||
		assert!(account_resp.terms_of_service_agreed.unwrap());
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			account_resp.orders,
 | 
				
			|||
			Some("https://example.com/acme/orders/rzGoeA".into())
 | 
				
			|||
		);
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_update() {
 | 
				
			|||
		let emails = vec![
 | 
				
			|||
			"mailto:derp@example.com".to_string(),
 | 
				
			|||
			"mailto:derp.derpson@example.com".to_string(),
 | 
				
			|||
		];
 | 
				
			|||
		let au = AccountUpdate::new(&emails);
 | 
				
			|||
		assert_eq!(au.contact.len(), 2);
 | 
				
			|||
		let au_str = serde_json::to_string(&au);
 | 
				
			|||
		assert!(au_str.is_ok());
 | 
				
			|||
		let au_str = au_str.unwrap();
 | 
				
			|||
		assert!(au_str.starts_with("{"));
 | 
				
			|||
		assert!(au_str.ends_with("}"));
 | 
				
			|||
		assert!(au_str.contains("\"contact\""));
 | 
				
			|||
		assert!(au_str.contains("\"mailto:derp@example.com\""));
 | 
				
			|||
		assert!(au_str.contains("\"mailto:derp.derpson@example.com\""));
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_account_deactivation() {
 | 
				
			|||
		let ad = AccountDeactivation::new();
 | 
				
			|||
		assert_eq!(ad.status, "deactivated");
 | 
				
			|||
		let ad_str = serde_json::to_string(&ad);
 | 
				
			|||
		assert!(ad_str.is_ok());
 | 
				
			|||
		let ad_str = ad_str.unwrap();
 | 
				
			|||
		assert!(ad_str.starts_with("{"));
 | 
				
			|||
		assert!(ad_str.ends_with("}"));
 | 
				
			|||
		assert!(ad_str.contains("\"status\""));
 | 
				
			|||
		assert!(ad_str.contains("\"deactivated\""));
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,349 +0,0 @@ | 
				
			|||
use crate::acme_proto::structs::{ApiError, HttpApiError, Identifier};
 | 
				
			|||
use acme_common::b64_encode;
 | 
				
			|||
use acme_common::crypto::{HashFunction, KeyPair};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::Deserialize;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
const ACME_OID: &str = "1.3.6.1.5.5.7.1";
 | 
				
			|||
const ID_PE_ACME_ID: usize = 31;
 | 
				
			|||
const DER_OCTET_STRING_ID: usize = 0x04;
 | 
				
			|||
const DER_STRUCT_NAME: &str = "DER";
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
pub struct Authorization {
 | 
				
			|||
	pub identifier: Identifier,
 | 
				
			|||
	pub status: AuthorizationStatus,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub expires: Option<String>,
 | 
				
			|||
	pub challenges: Vec<Challenge>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub wildcard: Option<bool>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl FromStr for Authorization {
 | 
				
			|||
	type Err = Error;
 | 
				
			|||
 | 
				
			|||
	fn from_str(data: &str) -> Result<Self, Self::Err> {
 | 
				
			|||
		let mut res: Self = serde_json::from_str(data)?;
 | 
				
			|||
		res.challenges.retain(|c| *c != Challenge::Unknown);
 | 
				
			|||
		Ok(res)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ApiError for Authorization {
 | 
				
			|||
	fn get_error(&self) -> Option<Error> {
 | 
				
			|||
		for challenge in self.challenges.iter() {
 | 
				
			|||
			let err = challenge.get_error();
 | 
				
			|||
			if err.is_some() {
 | 
				
			|||
				return err;
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		None
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Debug, PartialEq, Eq, Deserialize)]
 | 
				
			|||
#[serde(rename_all = "lowercase")]
 | 
				
			|||
pub enum AuthorizationStatus {
 | 
				
			|||
	Pending,
 | 
				
			|||
	Valid,
 | 
				
			|||
	Invalid,
 | 
				
			|||
	Deactivated,
 | 
				
			|||
	Expired,
 | 
				
			|||
	Revoked,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for AuthorizationStatus {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			AuthorizationStatus::Pending => "pending",
 | 
				
			|||
			AuthorizationStatus::Valid => "valid",
 | 
				
			|||
			AuthorizationStatus::Invalid => "invalid",
 | 
				
			|||
			AuthorizationStatus::Deactivated => "deactivated",
 | 
				
			|||
			AuthorizationStatus::Expired => "expired",
 | 
				
			|||
			AuthorizationStatus::Revoked => "revoked",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(PartialEq, Deserialize)]
 | 
				
			|||
#[serde(tag = "type")]
 | 
				
			|||
pub enum Challenge {
 | 
				
			|||
	#[serde(rename = "http-01")]
 | 
				
			|||
	Http01(TokenChallenge),
 | 
				
			|||
	#[serde(rename = "dns-01")]
 | 
				
			|||
	Dns01(TokenChallenge),
 | 
				
			|||
	#[serde(rename = "tls-alpn-01")]
 | 
				
			|||
	TlsAlpn01(TokenChallenge),
 | 
				
			|||
	#[serde(other)]
 | 
				
			|||
	Unknown,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
deserialize_from_str!(Challenge);
 | 
				
			|||
 | 
				
			|||
impl Challenge {
 | 
				
			|||
	pub fn get_url(&self) -> String {
 | 
				
			|||
		match self {
 | 
				
			|||
			Challenge::Http01(tc) | Challenge::Dns01(tc) | Challenge::TlsAlpn01(tc) => {
 | 
				
			|||
				tc.url.to_owned()
 | 
				
			|||
			}
 | 
				
			|||
			Challenge::Unknown => String::new(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_proof(&self, key_pair: &KeyPair) -> Result<(String, Option<String>), Error> {
 | 
				
			|||
		match self {
 | 
				
			|||
			Challenge::Http01(tc) => {
 | 
				
			|||
				let ka = tc.key_authorization(key_pair)?;
 | 
				
			|||
				Ok((ka, None))
 | 
				
			|||
			}
 | 
				
			|||
			Challenge::Dns01(tc) => {
 | 
				
			|||
				let ka = tc.key_authorization(key_pair)?;
 | 
				
			|||
				let a = HashFunction::Sha256.hash(ka.as_bytes());
 | 
				
			|||
				let a = b64_encode(&a);
 | 
				
			|||
				Ok((a, None))
 | 
				
			|||
			}
 | 
				
			|||
			Challenge::TlsAlpn01(tc) => {
 | 
				
			|||
				let acme_ext_name = format!("{ACME_OID}.{ID_PE_ACME_ID}");
 | 
				
			|||
				let ka = tc.key_authorization(key_pair)?;
 | 
				
			|||
				let proof = HashFunction::Sha256.hash(ka.as_bytes());
 | 
				
			|||
				let b64_hash = b64_encode(&proof);
 | 
				
			|||
				let proof_str = proof
 | 
				
			|||
					.iter()
 | 
				
			|||
					.map(|e| format!("{e:02x}"))
 | 
				
			|||
					.collect::<Vec<String>>()
 | 
				
			|||
					.join(":");
 | 
				
			|||
				let value = format!(
 | 
				
			|||
					"critical,{DER_STRUCT_NAME}:{DER_OCTET_STRING_ID:02x}:{:02x}:{proof_str}",
 | 
				
			|||
					proof.len(),
 | 
				
			|||
				);
 | 
				
			|||
				let acme_ext = format!("{acme_ext_name}={value}");
 | 
				
			|||
				Ok((acme_ext, Some(b64_hash)))
 | 
				
			|||
			}
 | 
				
			|||
			Challenge::Unknown => Ok((String::new(), None)),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_file_name(&self) -> String {
 | 
				
			|||
		match self {
 | 
				
			|||
			Challenge::Http01(tc) => tc.token.to_owned(),
 | 
				
			|||
			Challenge::Dns01(_) | Challenge::TlsAlpn01(_) => String::new(),
 | 
				
			|||
			Challenge::Unknown => String::new(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ApiError for Challenge {
 | 
				
			|||
	fn get_error(&self) -> Option<Error> {
 | 
				
			|||
		match self {
 | 
				
			|||
			Challenge::Http01(tc) | Challenge::Dns01(tc) | Challenge::TlsAlpn01(tc) => {
 | 
				
			|||
				tc.error.to_owned().map(Error::from)
 | 
				
			|||
			}
 | 
				
			|||
			Challenge::Unknown => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(PartialEq, Deserialize)]
 | 
				
			|||
pub struct TokenChallenge {
 | 
				
			|||
	pub url: String,
 | 
				
			|||
	pub status: Option<ChallengeStatus>,
 | 
				
			|||
	pub validated: Option<String>,
 | 
				
			|||
	pub error: Option<HttpApiError>,
 | 
				
			|||
	pub token: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl TokenChallenge {
 | 
				
			|||
	fn key_authorization(&self, key_pair: &KeyPair) -> Result<String, Error> {
 | 
				
			|||
		let thumbprint = key_pair.jwk_public_key_thumbprint()?;
 | 
				
			|||
		let thumbprint = HashFunction::Sha256.hash(thumbprint.to_string().as_bytes());
 | 
				
			|||
		let thumbprint = b64_encode(&thumbprint);
 | 
				
			|||
		let auth = format!("{}.{thumbprint}", self.token);
 | 
				
			|||
		Ok(auth)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Debug, PartialEq, Eq, Deserialize)]
 | 
				
			|||
#[serde(rename_all = "lowercase")]
 | 
				
			|||
pub enum ChallengeStatus {
 | 
				
			|||
	Pending,
 | 
				
			|||
	Processing,
 | 
				
			|||
	Valid,
 | 
				
			|||
	Invalid,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::{Authorization, AuthorizationStatus, Challenge, ChallengeStatus};
 | 
				
			|||
	use crate::identifier::IdentifierType;
 | 
				
			|||
	use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_authorization() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"status\": \"pending\",
 | 
				
			|||
	\"identifier\": {
 | 
				
			|||
		\"type\": \"dns\",
 | 
				
			|||
		\"value\": \"example.com\"
 | 
				
			|||
	},
 | 
				
			|||
	\"challenges\": []
 | 
				
			|||
}";
 | 
				
			|||
		let a = Authorization::from_str(data);
 | 
				
			|||
		assert!(a.is_ok());
 | 
				
			|||
		let a = a.unwrap();
 | 
				
			|||
		assert_eq!(a.status, AuthorizationStatus::Pending);
 | 
				
			|||
		assert!(a.challenges.is_empty());
 | 
				
			|||
		let i = a.identifier;
 | 
				
			|||
		assert_eq!(i.id_type, IdentifierType::Dns);
 | 
				
			|||
		assert_eq!(i.value, "example.com".to_string());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_authorization_challenge() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"status\": \"pending\",
 | 
				
			|||
	\"identifier\": {
 | 
				
			|||
		\"type\": \"dns\",
 | 
				
			|||
		\"value\": \"example.com\"
 | 
				
			|||
	},
 | 
				
			|||
	\"challenges\": [
 | 
				
			|||
		{
 | 
				
			|||
			\"type\": \"dns-01\",
 | 
				
			|||
			\"status\": \"pending\",
 | 
				
			|||
			\"url\": \"https://example.com/chall/jYWxob3N0OjE\",
 | 
				
			|||
			\"token\": \"1y9UVMUvkqQVljCsnwlRLsbJcwN9nx-qDd6JHzXQQsw\"
 | 
				
			|||
		}
 | 
				
			|||
	]
 | 
				
			|||
}";
 | 
				
			|||
		let a = Authorization::from_str(data);
 | 
				
			|||
		assert!(a.is_ok());
 | 
				
			|||
		let a = a.unwrap();
 | 
				
			|||
		assert_eq!(a.status, AuthorizationStatus::Pending);
 | 
				
			|||
		assert_eq!(a.challenges.len(), 1);
 | 
				
			|||
		let i = a.identifier;
 | 
				
			|||
		assert_eq!(i.id_type, IdentifierType::Dns);
 | 
				
			|||
		assert_eq!(i.value, "example.com".to_string());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_authorization_unknown_challenge() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"status\": \"pending\",
 | 
				
			|||
	\"identifier\": {
 | 
				
			|||
		\"type\": \"dns\",
 | 
				
			|||
		\"value\": \"example.com\"
 | 
				
			|||
	},
 | 
				
			|||
	\"challenges\": [
 | 
				
			|||
		{
 | 
				
			|||
			\"type\": \"invalid-challenge-01\",
 | 
				
			|||
			\"status\": \"pending\",
 | 
				
			|||
			\"url\": \"https://example.com/chall/jYWxob3N0OjE\",
 | 
				
			|||
			\"token\": \"1y9UVMUvkqQVljCsnwlRLsbJcwN9nx-qDd6JHzXQQsw\"
 | 
				
			|||
		}
 | 
				
			|||
	]
 | 
				
			|||
}";
 | 
				
			|||
		let a = Authorization::from_str(data);
 | 
				
			|||
		assert!(a.is_ok());
 | 
				
			|||
		let a = a.unwrap();
 | 
				
			|||
		assert_eq!(a.status, AuthorizationStatus::Pending);
 | 
				
			|||
		assert!(a.challenges.is_empty());
 | 
				
			|||
		let i = a.identifier;
 | 
				
			|||
		assert_eq!(i.id_type, IdentifierType::Dns);
 | 
				
			|||
		assert_eq!(i.value, "example.com".to_string());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_invalid_authorization() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"status\": \"pending\",
 | 
				
			|||
	\"identifier\": {
 | 
				
			|||
		\"type\": \"foo\",
 | 
				
			|||
		\"value\": \"bar\"
 | 
				
			|||
	},
 | 
				
			|||
	\"challenges\": []
 | 
				
			|||
}";
 | 
				
			|||
		let a = Authorization::from_str(data);
 | 
				
			|||
		assert!(a.is_err());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_http01_challenge() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"type\": \"http-01\",
 | 
				
			|||
	\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
 | 
				
			|||
	\"status\": \"pending\",
 | 
				
			|||
	\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
 | 
				
			|||
}";
 | 
				
			|||
		let challenge = Challenge::from_str(data);
 | 
				
			|||
		assert!(challenge.is_ok());
 | 
				
			|||
		let challenge = challenge.unwrap();
 | 
				
			|||
		let c = match challenge {
 | 
				
			|||
			Challenge::Http01(c) => c,
 | 
				
			|||
			_ => {
 | 
				
			|||
				assert!(false);
 | 
				
			|||
				return;
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			c.url,
 | 
				
			|||
			"https://example.com/acme/chall/prV_B7yEyA4".to_string()
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(c.status, Some(ChallengeStatus::Pending));
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			c.token,
 | 
				
			|||
			"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0".to_string()
 | 
				
			|||
		);
 | 
				
			|||
		assert!(c.validated.is_none());
 | 
				
			|||
		assert!(c.error.is_none());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_dns01_challenge() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"type\": \"http-01\",
 | 
				
			|||
	\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
 | 
				
			|||
	\"status\": \"valid\",
 | 
				
			|||
	\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
 | 
				
			|||
}";
 | 
				
			|||
		let challenge = Challenge::from_str(data);
 | 
				
			|||
		assert!(challenge.is_ok());
 | 
				
			|||
		let challenge = challenge.unwrap();
 | 
				
			|||
		let c = match challenge {
 | 
				
			|||
			Challenge::Http01(c) => c,
 | 
				
			|||
			_ => {
 | 
				
			|||
				assert!(false);
 | 
				
			|||
				return;
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			c.url,
 | 
				
			|||
			"https://example.com/acme/chall/prV_B7yEyA4".to_string()
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(c.status, Some(ChallengeStatus::Valid));
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			c.token,
 | 
				
			|||
			"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0".to_string()
 | 
				
			|||
		);
 | 
				
			|||
		assert!(c.validated.is_none());
 | 
				
			|||
		assert!(c.error.is_none());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_unknown_challenge_type() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"type\": \"invalid-01\",
 | 
				
			|||
	\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
 | 
				
			|||
	\"status\": \"pending\",
 | 
				
			|||
	\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
 | 
				
			|||
}";
 | 
				
			|||
		let challenge = Challenge::from_str(data);
 | 
				
			|||
		assert!(challenge.is_ok());
 | 
				
			|||
		match challenge.unwrap() {
 | 
				
			|||
			Challenge::Unknown => assert!(true),
 | 
				
			|||
			_ => assert!(false),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,151 +0,0 @@ | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::Deserialize;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
#[allow(dead_code)]
 | 
				
			|||
pub struct DirectoryMeta {
 | 
				
			|||
	pub terms_of_service: Option<String>,
 | 
				
			|||
	pub website: Option<String>,
 | 
				
			|||
	pub caa_identities: Option<Vec<String>>,
 | 
				
			|||
	pub external_account_required: Option<bool>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
pub struct Directory {
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub meta: Option<DirectoryMeta>,
 | 
				
			|||
	pub new_nonce: String,
 | 
				
			|||
	pub new_account: String,
 | 
				
			|||
	pub new_order: String,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub new_authz: Option<String>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub revoke_cert: String,
 | 
				
			|||
	pub key_change: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
deserialize_from_str!(Directory);
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::Directory;
 | 
				
			|||
	use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_directory() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"newAccount\": \"https://example.org/acme/new-acct\",
 | 
				
			|||
	\"newNonce\": \"https://example.org/acme/new-nonce\",
 | 
				
			|||
	\"newOrder\": \"https://example.org/acme/new-order\",
 | 
				
			|||
	\"revokeCert\": \"https://example.org/acme/revoke-cert\",
 | 
				
			|||
	\"newAuthz\": \"https://example.org/acme/new-authz\",
 | 
				
			|||
	\"keyChange\": \"https://example.org/acme/key-change\"
 | 
				
			|||
}";
 | 
				
			|||
		let parsed_dir = Directory::from_str(data);
 | 
				
			|||
		assert!(parsed_dir.is_ok());
 | 
				
			|||
		let parsed_dir = parsed_dir.unwrap();
 | 
				
			|||
		assert_eq!(parsed_dir.new_nonce, "https://example.org/acme/new-nonce");
 | 
				
			|||
		assert_eq!(parsed_dir.new_account, "https://example.org/acme/new-acct");
 | 
				
			|||
		assert_eq!(parsed_dir.new_order, "https://example.org/acme/new-order");
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			parsed_dir.new_authz,
 | 
				
			|||
			Some("https://example.org/acme/new-authz".to_string())
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			parsed_dir.revoke_cert,
 | 
				
			|||
			"https://example.org/acme/revoke-cert"
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(parsed_dir.key_change, "https://example.org/acme/key-change");
 | 
				
			|||
		assert!(parsed_dir.meta.is_none());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_directory_no_authz() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"newAccount\": \"https://example.org/acme/new-acct\",
 | 
				
			|||
	\"newNonce\": \"https://example.org/acme/new-nonce\",
 | 
				
			|||
	\"newOrder\": \"https://example.org/acme/new-order\",
 | 
				
			|||
	\"revokeCert\": \"https://example.org/acme/revoke-cert\",
 | 
				
			|||
	\"keyChange\": \"https://example.org/acme/key-change\"
 | 
				
			|||
}";
 | 
				
			|||
		let parsed_dir = Directory::from_str(data);
 | 
				
			|||
		assert!(parsed_dir.is_ok());
 | 
				
			|||
		let parsed_dir = parsed_dir.unwrap();
 | 
				
			|||
		assert_eq!(parsed_dir.new_nonce, "https://example.org/acme/new-nonce");
 | 
				
			|||
		assert_eq!(parsed_dir.new_account, "https://example.org/acme/new-acct");
 | 
				
			|||
		assert_eq!(parsed_dir.new_order, "https://example.org/acme/new-order");
 | 
				
			|||
		assert!(parsed_dir.new_authz.is_none());
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			parsed_dir.revoke_cert,
 | 
				
			|||
			"https://example.org/acme/revoke-cert"
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(parsed_dir.key_change, "https://example.org/acme/key-change");
 | 
				
			|||
		assert!(parsed_dir.meta.is_none());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_directory_meta() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"keyChange\": \"https://example.org/acme/key-change\",
 | 
				
			|||
	\"meta\": {
 | 
				
			|||
		\"caaIdentities\": [
 | 
				
			|||
			\"example.org\"
 | 
				
			|||
		],
 | 
				
			|||
		\"termsOfService\": \"https://example.org/documents/tos.pdf\",
 | 
				
			|||
		\"website\": \"https://example.org/\"
 | 
				
			|||
	},
 | 
				
			|||
	\"newAccount\": \"https://example.org/acme/new-acct\",
 | 
				
			|||
	\"newNonce\": \"https://example.org/acme/new-nonce\",
 | 
				
			|||
	\"newOrder\": \"https://example.org/acme/new-order\",
 | 
				
			|||
	\"revokeCert\": \"https://example.org/acme/revoke-cert\"
 | 
				
			|||
}";
 | 
				
			|||
		let parsed_dir = Directory::from_str(&data);
 | 
				
			|||
		assert!(parsed_dir.is_ok());
 | 
				
			|||
		let parsed_dir = parsed_dir.unwrap();
 | 
				
			|||
		assert!(parsed_dir.meta.is_some());
 | 
				
			|||
		let meta = parsed_dir.meta.unwrap();
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			meta.terms_of_service,
 | 
				
			|||
			Some("https://example.org/documents/tos.pdf".to_string())
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(meta.website, Some("https://example.org/".to_string()));
 | 
				
			|||
		assert!(meta.caa_identities.is_some());
 | 
				
			|||
		let caa_identities = meta.caa_identities.unwrap();
 | 
				
			|||
		assert_eq!(caa_identities.len(), 1);
 | 
				
			|||
		assert_eq!(caa_identities.first(), Some(&"example.org".to_string()));
 | 
				
			|||
		assert!(meta.external_account_required.is_none());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_directory_extra_fields() {
 | 
				
			|||
		let data = "{
 | 
				
			|||
	\"foo\": \"bar\",
 | 
				
			|||
	\"keyChange\": \"https://example.org/acme/key-change\",
 | 
				
			|||
	\"newAccount\": \"https://example.org/acme/new-acct\",
 | 
				
			|||
	\"baz\": \"quz\",
 | 
				
			|||
	\"newNonce\": \"https://example.org/acme/new-nonce\",
 | 
				
			|||
	\"newAuthz\": \"https://example.org/acme/new-authz\",
 | 
				
			|||
	\"newOrder\": \"https://example.org/acme/new-order\",
 | 
				
			|||
	\"revokeCert\": \"https://example.org/acme/revoke-cert\"
 | 
				
			|||
}";
 | 
				
			|||
		let parsed_dir = Directory::from_str(&data);
 | 
				
			|||
		assert!(parsed_dir.is_ok());
 | 
				
			|||
		let parsed_dir = parsed_dir.unwrap();
 | 
				
			|||
		assert_eq!(parsed_dir.new_nonce, "https://example.org/acme/new-nonce");
 | 
				
			|||
		assert_eq!(parsed_dir.new_account, "https://example.org/acme/new-acct");
 | 
				
			|||
		assert_eq!(parsed_dir.new_order, "https://example.org/acme/new-order");
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			parsed_dir.new_authz,
 | 
				
			|||
			Some("https://example.org/acme/new-authz".to_string())
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			parsed_dir.revoke_cert,
 | 
				
			|||
			"https://example.org/acme/revoke-cert"
 | 
				
			|||
		);
 | 
				
			|||
		assert_eq!(parsed_dir.key_change, "https://example.org/acme/key-change");
 | 
				
			|||
		assert!(parsed_dir.meta.is_none());
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,173 +0,0 @@ | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::Deserialize;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
pub trait ApiError {
 | 
				
			|||
	fn get_error(&self) -> Option<Error>;
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, PartialEq)]
 | 
				
			|||
pub enum AcmeError {
 | 
				
			|||
	AccountDoesNotExist,
 | 
				
			|||
	AlreadyRevoked,
 | 
				
			|||
	BadCSR,
 | 
				
			|||
	BadNonce,
 | 
				
			|||
	BadPublicKey,
 | 
				
			|||
	BadRevocationReason,
 | 
				
			|||
	BadSignatureAlgorithm,
 | 
				
			|||
	Caa,
 | 
				
			|||
	Compound,
 | 
				
			|||
	Connection,
 | 
				
			|||
	Dns,
 | 
				
			|||
	ExternalAccountRequired,
 | 
				
			|||
	IncorrectResponse,
 | 
				
			|||
	InvalidContact,
 | 
				
			|||
	Malformed,
 | 
				
			|||
	OrderNotReady,
 | 
				
			|||
	RateLimited,
 | 
				
			|||
	RejectedIdentifier,
 | 
				
			|||
	ServerInternal,
 | 
				
			|||
	Tls,
 | 
				
			|||
	Unauthorized,
 | 
				
			|||
	UnsupportedContact,
 | 
				
			|||
	UnsupportedIdentifier,
 | 
				
			|||
	UserActionRequired,
 | 
				
			|||
	Unknown,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<String> for AcmeError {
 | 
				
			|||
	fn from(error: String) -> Self {
 | 
				
			|||
		match error.as_str() {
 | 
				
			|||
			"urn:ietf:params:acme:error:accountDoesNotExist" => AcmeError::AccountDoesNotExist,
 | 
				
			|||
			"urn:ietf:params:acme:error:alreadyRevoked" => AcmeError::AlreadyRevoked,
 | 
				
			|||
			"urn:ietf:params:acme:error:badCSR" => AcmeError::BadCSR,
 | 
				
			|||
			"urn:ietf:params:acme:error:badNonce" => AcmeError::BadNonce,
 | 
				
			|||
			"urn:ietf:params:acme:error:badPublicKey" => AcmeError::BadPublicKey,
 | 
				
			|||
			"urn:ietf:params:acme:error:badRevocationReason" => AcmeError::BadRevocationReason,
 | 
				
			|||
			"urn:ietf:params:acme:error:badSignatureAlgorithm" => AcmeError::BadSignatureAlgorithm,
 | 
				
			|||
			"urn:ietf:params:acme:error:caa" => AcmeError::Caa,
 | 
				
			|||
			"urn:ietf:params:acme:error:compound" => AcmeError::Compound,
 | 
				
			|||
			"urn:ietf:params:acme:error:connection" => AcmeError::Connection,
 | 
				
			|||
			"urn:ietf:params:acme:error:dns" => AcmeError::Dns,
 | 
				
			|||
			"urn:ietf:params:acme:error:externalAccountRequired" => {
 | 
				
			|||
				AcmeError::ExternalAccountRequired
 | 
				
			|||
			}
 | 
				
			|||
			"urn:ietf:params:acme:error:incorrectResponse" => AcmeError::IncorrectResponse,
 | 
				
			|||
			"urn:ietf:params:acme:error:invalidContact" => AcmeError::InvalidContact,
 | 
				
			|||
			"urn:ietf:params:acme:error:malformed" => AcmeError::Malformed,
 | 
				
			|||
			"urn:ietf:params:acme:error:orderNotReady" => AcmeError::OrderNotReady,
 | 
				
			|||
			"urn:ietf:params:acme:error:rateLimited" => AcmeError::RateLimited,
 | 
				
			|||
			"urn:ietf:params:acme:error:rejectedIdentifier" => AcmeError::RejectedIdentifier,
 | 
				
			|||
			"urn:ietf:params:acme:error:serverInternal" => AcmeError::ServerInternal,
 | 
				
			|||
			"urn:ietf:params:acme:error:tls" => AcmeError::Tls,
 | 
				
			|||
			"urn:ietf:params:acme:error:unauthorized" => AcmeError::Unauthorized,
 | 
				
			|||
			"urn:ietf:params:acme:error:unsupportedContact" => AcmeError::UnsupportedContact,
 | 
				
			|||
			"urn:ietf:params:acme:error:unsupportedIdentifier" => AcmeError::UnsupportedIdentifier,
 | 
				
			|||
			"urn:ietf:params:acme:error:userActionRequired" => AcmeError::UserActionRequired,
 | 
				
			|||
			_ => AcmeError::Unknown,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for AcmeError {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let msg = match self {
 | 
				
			|||
			AcmeError::AccountDoesNotExist => "the request specified an account that does not exist",
 | 
				
			|||
			AcmeError::AlreadyRevoked => "the request specified a certificate to be revoked that has already been revoked",
 | 
				
			|||
			AcmeError::BadCSR => "the CSR is unacceptable (e.g., due to a short key)",
 | 
				
			|||
			AcmeError::BadNonce => "the client sent an unacceptable anti-replay nonce",
 | 
				
			|||
			AcmeError::BadPublicKey => "the JWS was signed by a public key the server does not support",
 | 
				
			|||
			AcmeError::BadRevocationReason => "the revocation reason provided is not allowed by the server",
 | 
				
			|||
			AcmeError::BadSignatureAlgorithm => "the JWS was signed with an algorithm the server does not support",
 | 
				
			|||
			AcmeError::Caa => "Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate",
 | 
				
			|||
			AcmeError::Compound => "specific error conditions are indicated in the \"subproblems\" array",
 | 
				
			|||
			AcmeError::Connection => "the server could not connect to validation target",
 | 
				
			|||
			AcmeError::Dns => "there was a problem with a DNS query during identifier validation",
 | 
				
			|||
			AcmeError::ExternalAccountRequired => "the request must include a value for the \"externalAccountBinding\" field",
 | 
				
			|||
			AcmeError::IncorrectResponse => "response received didn't match the challenge's requirements",
 | 
				
			|||
			AcmeError::InvalidContact => "a contact URL for an account was invalid",
 | 
				
			|||
			AcmeError::Malformed => "the request message was malformed",
 | 
				
			|||
			AcmeError::OrderNotReady => "the request attempted to finalize an order that is not ready to be finalized",
 | 
				
			|||
			AcmeError::RateLimited => "the request exceeds a rate limit",
 | 
				
			|||
			AcmeError::RejectedIdentifier => "the server will not issue certificates for the identifier",
 | 
				
			|||
			AcmeError::ServerInternal => "the server experienced an internal error",
 | 
				
			|||
			AcmeError::Tls => "the server received a TLS error during validation",
 | 
				
			|||
			AcmeError::Unauthorized => "the client lacks sufficient authorization",
 | 
				
			|||
			AcmeError::UnsupportedContact => "a contact URL for an account used an unsupported protocol scheme",
 | 
				
			|||
			AcmeError::UnsupportedIdentifier => "an identifier is of an unsupported type",
 | 
				
			|||
			AcmeError::UserActionRequired => "visit the \"instance\" URL and take actions specified there",
 | 
				
			|||
			AcmeError::Unknown => "unknown error",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{msg}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AcmeError {
 | 
				
			|||
	pub fn is_recoverable(&self) -> bool {
 | 
				
			|||
		*self == AcmeError::BadNonce
 | 
				
			|||
			|| *self == AcmeError::Connection
 | 
				
			|||
			|| *self == AcmeError::Dns
 | 
				
			|||
			|| *self == AcmeError::Malformed
 | 
				
			|||
			|| *self == AcmeError::RateLimited
 | 
				
			|||
			|| *self == AcmeError::ServerInternal
 | 
				
			|||
			|| *self == AcmeError::Tls
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<Error> for AcmeError {
 | 
				
			|||
	fn from(_error: Error) -> Self {
 | 
				
			|||
		AcmeError::Unknown
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<AcmeError> for Error {
 | 
				
			|||
	fn from(error: AcmeError) -> Self {
 | 
				
			|||
		error.to_string().into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, PartialEq, Deserialize)]
 | 
				
			|||
pub struct HttpApiError {
 | 
				
			|||
	#[serde(rename = "type")]
 | 
				
			|||
	error_type: Option<String>,
 | 
				
			|||
	// title: Option<String>,
 | 
				
			|||
	status: Option<usize>,
 | 
				
			|||
	detail: Option<String>,
 | 
				
			|||
	// instance: Option<String>,
 | 
				
			|||
	// TODO: implement subproblems
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
crate::acme_proto::structs::deserialize_from_str!(HttpApiError);
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for HttpApiError {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let msg = self
 | 
				
			|||
			.detail
 | 
				
			|||
			.to_owned()
 | 
				
			|||
			.unwrap_or_else(|| self.get_acme_type().to_string());
 | 
				
			|||
		let msg = match self.status {
 | 
				
			|||
			Some(s) => format!("status {s}: {msg}"),
 | 
				
			|||
			None => msg,
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{msg}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl HttpApiError {
 | 
				
			|||
	pub fn get_type(&self) -> String {
 | 
				
			|||
		self.error_type
 | 
				
			|||
			.to_owned()
 | 
				
			|||
			.unwrap_or_else(|| String::from("about:blank"))
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_acme_type(&self) -> AcmeError {
 | 
				
			|||
		self.get_type().into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<HttpApiError> for Error {
 | 
				
			|||
	fn from(error: HttpApiError) -> Self {
 | 
				
			|||
		error.to_string().into()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,135 +0,0 @@ | 
				
			|||
use crate::acme_proto::structs::{ApiError, HttpApiError};
 | 
				
			|||
use crate::identifier::{self, IdentifierType};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::{Deserialize, Serialize};
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
pub struct NewOrder {
 | 
				
			|||
	pub identifiers: Vec<Identifier>,
 | 
				
			|||
	#[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			|||
	pub not_before: Option<String>,
 | 
				
			|||
	#[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			|||
	pub not_after: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl NewOrder {
 | 
				
			|||
	pub fn new(identifiers: &[identifier::Identifier]) -> Self {
 | 
				
			|||
		NewOrder {
 | 
				
			|||
			identifiers: identifiers.iter().map(Identifier::from_generic).collect(),
 | 
				
			|||
			not_before: None,
 | 
				
			|||
			not_after: None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
#[serde(rename_all = "camelCase")]
 | 
				
			|||
pub struct Order {
 | 
				
			|||
	pub status: OrderStatus,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub expires: Option<String>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub identifiers: Vec<Identifier>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub not_before: Option<String>,
 | 
				
			|||
	#[allow(dead_code)]
 | 
				
			|||
	pub not_after: Option<String>,
 | 
				
			|||
	pub error: Option<HttpApiError>,
 | 
				
			|||
	pub authorizations: Vec<String>,
 | 
				
			|||
	pub finalize: String,
 | 
				
			|||
	pub certificate: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ApiError for Order {
 | 
				
			|||
	fn get_error(&self) -> Option<Error> {
 | 
				
			|||
		self.error.to_owned().map(Error::from)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
deserialize_from_str!(Order);
 | 
				
			|||
 | 
				
			|||
#[derive(Debug, PartialEq, Eq, Deserialize)]
 | 
				
			|||
#[serde(rename_all = "lowercase")]
 | 
				
			|||
pub enum OrderStatus {
 | 
				
			|||
	Pending,
 | 
				
			|||
	Ready,
 | 
				
			|||
	Processing,
 | 
				
			|||
	Valid,
 | 
				
			|||
	Invalid,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for OrderStatus {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			OrderStatus::Pending => "pending",
 | 
				
			|||
			OrderStatus::Ready => "ready",
 | 
				
			|||
			OrderStatus::Processing => "processing",
 | 
				
			|||
			OrderStatus::Valid => "valid",
 | 
				
			|||
			OrderStatus::Invalid => "invalid",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize, Serialize)]
 | 
				
			|||
pub struct Identifier {
 | 
				
			|||
	#[serde(rename = "type")]
 | 
				
			|||
	pub id_type: IdentifierType,
 | 
				
			|||
	pub value: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Identifier {
 | 
				
			|||
	pub fn from_generic(id: &identifier::Identifier) -> Self {
 | 
				
			|||
		Identifier {
 | 
				
			|||
			id_type: id.id_type.to_owned(),
 | 
				
			|||
			value: id.value.to_owned(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
deserialize_from_str!(Identifier);
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Identifier {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		write!(f, "{}:{}", self.id_type, self.value)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::{Identifier, IdentifierType};
 | 
				
			|||
	use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn id_serialize() {
 | 
				
			|||
		let reference = "{\"type\":\"dns\",\"value\":\"test.example.org\"}";
 | 
				
			|||
		let id = Identifier {
 | 
				
			|||
			id_type: IdentifierType::Dns,
 | 
				
			|||
			value: "test.example.org".to_string(),
 | 
				
			|||
		};
 | 
				
			|||
		let id_json = serde_json::to_string(&id);
 | 
				
			|||
		assert!(id_json.is_ok());
 | 
				
			|||
		let id_json = id_json.unwrap();
 | 
				
			|||
		assert_eq!(id_json, reference.to_string());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn id_deserialize_valid() {
 | 
				
			|||
		let id_str = "{\"type\":\"dns\",\"value\":\"test.example.org\"}";
 | 
				
			|||
		let id = Identifier::from_str(id_str);
 | 
				
			|||
		assert!(id.is_ok());
 | 
				
			|||
		let id = id.unwrap();
 | 
				
			|||
		assert_eq!(id.id_type, IdentifierType::Dns);
 | 
				
			|||
		assert_eq!(id.value, "test.example.org".to_string());
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn id_deserialize_invalid_type() {
 | 
				
			|||
		let id_str = "{\"type\":\"trololo\",\"value\":\"test.example.org\"}";
 | 
				
			|||
		let id = Identifier::from_str(id_str);
 | 
				
			|||
		assert!(id.is_err());
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,203 +0,0 @@ | 
				
			|||
use crate::acme_proto::Challenge;
 | 
				
			|||
use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, HookType, PostOperationHookData};
 | 
				
			|||
use crate::identifier::{Identifier, IdentifierType};
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::storage::{certificate_files_exists, get_certificate, FileManager};
 | 
				
			|||
use acme_common::crypto::{HashFunction, KeyType, SubjectAttribute, X509Certificate};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use log::{debug, info, trace, warn};
 | 
				
			|||
use rand::{thread_rng, Rng};
 | 
				
			|||
use std::collections::{HashMap, HashSet};
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::time::Duration;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct Certificate {
 | 
				
			|||
	pub account_name: String,
 | 
				
			|||
	pub identifiers: Vec<Identifier>,
 | 
				
			|||
	pub subject_attributes: HashMap<SubjectAttribute, String>,
 | 
				
			|||
	pub key_type: KeyType,
 | 
				
			|||
	pub csr_digest: HashFunction,
 | 
				
			|||
	pub kp_reuse: bool,
 | 
				
			|||
	pub endpoint_name: String,
 | 
				
			|||
	pub hooks: Vec<Hook>,
 | 
				
			|||
	pub crt_name: String,
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
	pub random_early_renew: Duration,
 | 
				
			|||
	pub renew_delay: Duration,
 | 
				
			|||
	pub file_manager: FileManager,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Certificate {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		write!(f, "{}", self.get_id())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl HasLogger for Certificate {
 | 
				
			|||
	fn warn(&self, msg: &str) {
 | 
				
			|||
		warn!("certificate \"{self}\": {msg}");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn info(&self, msg: &str) {
 | 
				
			|||
		info!("certificate \"{self}\": {msg}");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn debug(&self, msg: &str) {
 | 
				
			|||
		debug!("certificate \"{self}\": {msg}");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn trace(&self, msg: &str) {
 | 
				
			|||
		trace!("certificate \"{self}\": {msg}");
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Certificate {
 | 
				
			|||
	pub fn get_id(&self) -> String {
 | 
				
			|||
		format!("{}_{}", self.crt_name, self.key_type)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_identifier_from_str(&self, identifier: &str) -> Result<Identifier, Error> {
 | 
				
			|||
		let identifier = identifier.to_string();
 | 
				
			|||
		for d in self.identifiers.iter() {
 | 
				
			|||
			let val = match d.id_type {
 | 
				
			|||
				// strip wildcards from domain before matching
 | 
				
			|||
				IdentifierType::Dns => d.value.trim_start_matches("*.").to_string(),
 | 
				
			|||
				IdentifierType::Ip => d.value.to_owned(),
 | 
				
			|||
			};
 | 
				
			|||
			if identifier == val {
 | 
				
			|||
				return Ok(d.clone());
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		Err(format!("{identifier}: identifier not found").into())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn renew_in(&self, cert: &X509Certificate) -> Result<Duration, Error> {
 | 
				
			|||
		let expires_in = cert.expires_in()?;
 | 
				
			|||
		self.debug(&format!(
 | 
				
			|||
			"certificate expires in {} days ({} days delay)",
 | 
				
			|||
			expires_in.as_secs() / 86400,
 | 
				
			|||
			self.renew_delay.as_secs() / 86400,
 | 
				
			|||
		));
 | 
				
			|||
		let expires_in = expires_in.saturating_sub(self.renew_delay);
 | 
				
			|||
		let expires_in = if !self.random_early_renew.is_zero() {
 | 
				
			|||
			expires_in
 | 
				
			|||
				.saturating_sub(thread_rng().gen_range(Duration::ZERO..self.random_early_renew))
 | 
				
			|||
		} else {
 | 
				
			|||
			expires_in
 | 
				
			|||
		};
 | 
				
			|||
		Ok(expires_in)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn has_missing_identifiers(&self, cert: &X509Certificate) -> bool {
 | 
				
			|||
		let cert_names = cert.subject_alt_names();
 | 
				
			|||
		let req_names = self
 | 
				
			|||
			.identifiers
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|v| v.value.to_owned())
 | 
				
			|||
			.collect::<HashSet<String>>();
 | 
				
			|||
		let has_miss = req_names.difference(&cert_names).count() != 0;
 | 
				
			|||
		if has_miss {
 | 
				
			|||
			let domains = req_names
 | 
				
			|||
				.difference(&cert_names)
 | 
				
			|||
				.map(std::borrow::ToOwned::to_owned)
 | 
				
			|||
				.collect::<Vec<String>>()
 | 
				
			|||
				.join(", ");
 | 
				
			|||
			self.debug(&format!(
 | 
				
			|||
				"the certificate does not include the following domains: {domains}"
 | 
				
			|||
			));
 | 
				
			|||
		}
 | 
				
			|||
		has_miss
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	/// Return a comma-separated list of the domains this certificate is valid for.
 | 
				
			|||
	pub fn identifier_list(&self) -> String {
 | 
				
			|||
		self.identifiers
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|d| d.value.as_str())
 | 
				
			|||
			.collect::<Vec<&str>>()
 | 
				
			|||
			.join(",")
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn schedule_renewal(&self) -> Result<Duration, Error> {
 | 
				
			|||
		self.debug(&format!(
 | 
				
			|||
			"checking for renewal (identifiers: {})",
 | 
				
			|||
			self.identifier_list()
 | 
				
			|||
		));
 | 
				
			|||
		if !certificate_files_exists(&self.file_manager) {
 | 
				
			|||
			self.debug("certificate does not exist: requesting one");
 | 
				
			|||
			return Ok(Duration::ZERO);
 | 
				
			|||
		}
 | 
				
			|||
		let cert = get_certificate(&self.file_manager).await?;
 | 
				
			|||
 | 
				
			|||
		if self.has_missing_identifiers(&cert) {
 | 
				
			|||
			self.debug("the current certificate doesn't include all the required identifiers");
 | 
				
			|||
			return Ok(Duration::ZERO);
 | 
				
			|||
		}
 | 
				
			|||
		self.renew_in(&cert)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn call_challenge_hooks(
 | 
				
			|||
		&self,
 | 
				
			|||
		file_name: &str,
 | 
				
			|||
		proof: &str,
 | 
				
			|||
		raw_proof: Option<String>,
 | 
				
			|||
		identifier: &str,
 | 
				
			|||
	) -> Result<(ChallengeHookData, HookType), Error> {
 | 
				
			|||
		let identifier = self.get_identifier_from_str(identifier)?;
 | 
				
			|||
		let mut hook_data = ChallengeHookData {
 | 
				
			|||
			challenge: identifier.challenge.to_string(),
 | 
				
			|||
			identifier: identifier.value.to_owned(),
 | 
				
			|||
			identifier_tls_alpn: identifier.get_tls_alpn_name().unwrap_or_default(),
 | 
				
			|||
			file_name: file_name.to_string(),
 | 
				
			|||
			proof: proof.to_string(),
 | 
				
			|||
			raw_proof: raw_proof.unwrap_or_default().to_string(),
 | 
				
			|||
			is_clean_hook: false,
 | 
				
			|||
			env: HashMap::new(),
 | 
				
			|||
		};
 | 
				
			|||
		hook_data.set_env(&self.env);
 | 
				
			|||
		hook_data.set_env(&identifier.env);
 | 
				
			|||
		let hook_type = match identifier.challenge {
 | 
				
			|||
			Challenge::Http01 => (HookType::ChallengeHttp01, HookType::ChallengeHttp01Clean),
 | 
				
			|||
			Challenge::Dns01 => (HookType::ChallengeDns01, HookType::ChallengeDns01Clean),
 | 
				
			|||
			Challenge::TlsAlpn01 => (
 | 
				
			|||
				HookType::ChallengeTlsAlpn01,
 | 
				
			|||
				HookType::ChallengeTlsAlpn01Clean,
 | 
				
			|||
			),
 | 
				
			|||
		};
 | 
				
			|||
		hooks::call(self, &self.hooks, &hook_data, hook_type.0).await?;
 | 
				
			|||
		Ok((hook_data, hook_type.1))
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn call_challenge_hooks_clean(
 | 
				
			|||
		&self,
 | 
				
			|||
		data: &ChallengeHookData,
 | 
				
			|||
		hook_type: HookType,
 | 
				
			|||
	) -> Result<(), Error> {
 | 
				
			|||
		hooks::call(self, &self.hooks, data, hook_type).await
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn call_post_operation_hooks(
 | 
				
			|||
		&self,
 | 
				
			|||
		status: &str,
 | 
				
			|||
		is_success: bool,
 | 
				
			|||
	) -> Result<(), Error> {
 | 
				
			|||
		let identifiers = self
 | 
				
			|||
			.identifiers
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|d| d.value.to_owned())
 | 
				
			|||
			.collect::<Vec<String>>();
 | 
				
			|||
		let mut hook_data = PostOperationHookData {
 | 
				
			|||
			identifiers,
 | 
				
			|||
			key_type: self.key_type.to_string(),
 | 
				
			|||
			status: status.to_string(),
 | 
				
			|||
			is_success,
 | 
				
			|||
			certificate_path: crate::storage::get_certificate_path(&self.file_manager).await?,
 | 
				
			|||
			private_key_path: crate::storage::get_keypair_path(&self.file_manager).await?,
 | 
				
			|||
			env: HashMap::new(),
 | 
				
			|||
		};
 | 
				
			|||
		hook_data.set_env(&self.env);
 | 
				
			|||
		hooks::call(self, &self.hooks, &hook_data, HookType::PostOperation).await?;
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,797 +0,0 @@ | 
				
			|||
use crate::duration::parse_duration;
 | 
				
			|||
use crate::hooks;
 | 
				
			|||
use crate::identifier::IdentifierType;
 | 
				
			|||
use crate::storage::FileManager;
 | 
				
			|||
use acme_common::b64_decode;
 | 
				
			|||
use acme_common::crypto::{HashFunction, JwsSignatureAlgorithm, KeyType, SubjectAttribute};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use glob::glob;
 | 
				
			|||
use log::info;
 | 
				
			|||
use serde::{de, Deserialize, Deserializer};
 | 
				
			|||
use std::collections::{BTreeSet, HashMap};
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::fs::{self, File};
 | 
				
			|||
use std::io::prelude::*;
 | 
				
			|||
use std::path::{Path, PathBuf};
 | 
				
			|||
use std::result::Result;
 | 
				
			|||
use std::time::Duration;
 | 
				
			|||
 | 
				
			|||
macro_rules! set_cfg_attr {
 | 
				
			|||
	($to: expr, $from: expr) => {
 | 
				
			|||
		if let Some(v) = $from {
 | 
				
			|||
			$to = Some(v);
 | 
				
			|||
		};
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! push_subject_attr {
 | 
				
			|||
	($hm: expr, $attr: expr, $attr_type: ident) => {
 | 
				
			|||
		if let Some(v) = &$attr {
 | 
				
			|||
			$hm.insert(SubjectAttribute::$attr_type, v.to_owned());
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_stdin(hook: &Hook) -> Result<hooks::HookStdin, Error> {
 | 
				
			|||
	match &hook.stdin {
 | 
				
			|||
		Some(file) => match &hook.stdin_str {
 | 
				
			|||
			Some(_) => {
 | 
				
			|||
				let msg = format!(
 | 
				
			|||
					"{}: a hook cannot have both stdin and stdin_str",
 | 
				
			|||
					&hook.name
 | 
				
			|||
				);
 | 
				
			|||
				Err(msg.into())
 | 
				
			|||
			}
 | 
				
			|||
			None => Ok(hooks::HookStdin::File(file.to_string())),
 | 
				
			|||
		},
 | 
				
			|||
		None => match &hook.stdin_str {
 | 
				
			|||
			Some(s) => Ok(hooks::HookStdin::Str(s.to_string())),
 | 
				
			|||
			None => Ok(hooks::HookStdin::None),
 | 
				
			|||
		},
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Default, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Config {
 | 
				
			|||
	pub global: Option<GlobalOptions>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub endpoint: Vec<Endpoint>,
 | 
				
			|||
	#[serde(default, rename = "rate-limit")]
 | 
				
			|||
	pub rate_limit: Vec<RateLimit>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub hook: Vec<Hook>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub group: Vec<Group>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub account: Vec<Account>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub certificate: Vec<Certificate>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub include: Vec<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Config {
 | 
				
			|||
	fn get_rate_limit(&self, name: &str) -> Result<(usize, String), Error> {
 | 
				
			|||
		for rl in self.rate_limit.iter() {
 | 
				
			|||
			if rl.name == name {
 | 
				
			|||
				return Ok((rl.number, rl.period.to_owned()));
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		Err(format!("{name}: rate limit not found").into())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_account_dir(&self) -> String {
 | 
				
			|||
		let account_dir = match &self.global {
 | 
				
			|||
			Some(g) => match &g.accounts_directory {
 | 
				
			|||
				Some(d) => d,
 | 
				
			|||
				None => crate::DEFAULT_ACCOUNTS_DIR,
 | 
				
			|||
			},
 | 
				
			|||
			None => crate::DEFAULT_ACCOUNTS_DIR,
 | 
				
			|||
		};
 | 
				
			|||
		account_dir.to_string()
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_hook(&self, name: &str) -> Result<Vec<hooks::Hook>, Error> {
 | 
				
			|||
		for hook in self.hook.iter() {
 | 
				
			|||
			if name == hook.name {
 | 
				
			|||
				let h = hooks::Hook {
 | 
				
			|||
					name: hook.name.to_owned(),
 | 
				
			|||
					hook_type: hook.hook_type.iter().map(|e| e.to_owned()).collect(),
 | 
				
			|||
					cmd: hook.cmd.to_owned(),
 | 
				
			|||
					args: hook.args.to_owned(),
 | 
				
			|||
					stdin: get_stdin(hook)?,
 | 
				
			|||
					stdout: hook.stdout.to_owned(),
 | 
				
			|||
					stderr: hook.stderr.to_owned(),
 | 
				
			|||
					allow_failure: hook
 | 
				
			|||
						.allow_failure
 | 
				
			|||
						.unwrap_or(crate::DEFAULT_HOOK_ALLOW_FAILURE),
 | 
				
			|||
				};
 | 
				
			|||
				return Ok(vec![h]);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		for grp in self.group.iter() {
 | 
				
			|||
			if name == grp.name {
 | 
				
			|||
				let mut ret = vec![];
 | 
				
			|||
				for hook_name in grp.hooks.iter() {
 | 
				
			|||
					let mut h = self.get_hook(hook_name)?;
 | 
				
			|||
					ret.append(&mut h);
 | 
				
			|||
				}
 | 
				
			|||
				return Ok(ret);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		Err(format!("{name}: hook not found").into())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_cert_file_mode(&self) -> u32 {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => match g.cert_file_mode {
 | 
				
			|||
				Some(m) => m,
 | 
				
			|||
				None => crate::DEFAULT_CERT_FILE_MODE,
 | 
				
			|||
			},
 | 
				
			|||
			None => crate::DEFAULT_CERT_FILE_MODE,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_cert_file_user(&self) -> Option<String> {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => g.cert_file_user.to_owned(),
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_cert_file_group(&self) -> Option<String> {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => g.cert_file_group.to_owned(),
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_cert_file_ext(&self) -> Option<String> {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => g.cert_file_ext.to_owned(),
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_pk_file_mode(&self) -> u32 {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => match g.pk_file_mode {
 | 
				
			|||
				Some(m) => m,
 | 
				
			|||
				None => crate::DEFAULT_PK_FILE_MODE,
 | 
				
			|||
			},
 | 
				
			|||
			None => crate::DEFAULT_PK_FILE_MODE,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_pk_file_user(&self) -> Option<String> {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => g.pk_file_user.to_owned(),
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_pk_file_group(&self) -> Option<String> {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => g.pk_file_group.to_owned(),
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_pk_file_ext(&self) -> Option<String> {
 | 
				
			|||
		match &self.global {
 | 
				
			|||
			Some(g) => g.pk_file_ext.to_owned(),
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct GlobalOptions {
 | 
				
			|||
	pub accounts_directory: Option<String>,
 | 
				
			|||
	pub cert_file_group: Option<String>,
 | 
				
			|||
	pub cert_file_mode: Option<u32>,
 | 
				
			|||
	pub cert_file_user: Option<String>,
 | 
				
			|||
	pub cert_file_ext: Option<String>,
 | 
				
			|||
	pub certificates_directory: Option<String>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
	pub file_name_format: Option<String>,
 | 
				
			|||
	pub pk_file_group: Option<String>,
 | 
				
			|||
	pub pk_file_mode: Option<u32>,
 | 
				
			|||
	pub pk_file_user: Option<String>,
 | 
				
			|||
	pub pk_file_ext: Option<String>,
 | 
				
			|||
	pub random_early_renew: Option<String>,
 | 
				
			|||
	pub renew_delay: Option<String>,
 | 
				
			|||
	pub root_certificates: Option<Vec<String>>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl GlobalOptions {
 | 
				
			|||
	pub fn get_random_early_renew(&self) -> Result<Duration, Error> {
 | 
				
			|||
		match &self.random_early_renew {
 | 
				
			|||
			Some(d) => parse_duration(d),
 | 
				
			|||
			None => Ok(Duration::new(crate::DEFAULT_CERT_RANDOM_EARLY_RENEW, 0)),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_renew_delay(&self) -> Result<Duration, Error> {
 | 
				
			|||
		match &self.renew_delay {
 | 
				
			|||
			Some(d) => parse_duration(d),
 | 
				
			|||
			None => Ok(Duration::new(crate::DEFAULT_CERT_RENEW_DELAY, 0)),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_crt_name_format(&self) -> String {
 | 
				
			|||
		match &self.file_name_format {
 | 
				
			|||
			Some(n) => n.to_string(),
 | 
				
			|||
			None => crate::DEFAULT_CERT_FORMAT.to_string(),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Endpoint {
 | 
				
			|||
	pub file_name_format: Option<String>,
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub random_early_renew: Option<String>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub rate_limits: Vec<String>,
 | 
				
			|||
	pub renew_delay: Option<String>,
 | 
				
			|||
	pub root_certificates: Option<Vec<String>>,
 | 
				
			|||
	pub tos_agreed: bool,
 | 
				
			|||
	pub url: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Endpoint {
 | 
				
			|||
	pub fn get_random_early_renew(&self, cnf: &Config) -> Result<Duration, Error> {
 | 
				
			|||
		match &self.random_early_renew {
 | 
				
			|||
			Some(d) => parse_duration(d),
 | 
				
			|||
			None => match &cnf.global {
 | 
				
			|||
				Some(g) => g.get_random_early_renew(),
 | 
				
			|||
				None => Ok(Duration::new(crate::DEFAULT_CERT_RANDOM_EARLY_RENEW, 0)),
 | 
				
			|||
			},
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_renew_delay(&self, cnf: &Config) -> Result<Duration, Error> {
 | 
				
			|||
		match &self.renew_delay {
 | 
				
			|||
			Some(d) => parse_duration(d),
 | 
				
			|||
			None => match &cnf.global {
 | 
				
			|||
				Some(g) => g.get_renew_delay(),
 | 
				
			|||
				None => Ok(Duration::new(crate::DEFAULT_CERT_RENEW_DELAY, 0)),
 | 
				
			|||
			},
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_crt_name_format(&self, cnf: &Config) -> String {
 | 
				
			|||
		match &self.file_name_format {
 | 
				
			|||
			Some(n) => n.to_string(),
 | 
				
			|||
			None => match &cnf.global {
 | 
				
			|||
				Some(g) => g.get_crt_name_format(),
 | 
				
			|||
				None => crate::DEFAULT_CERT_FORMAT.to_string(),
 | 
				
			|||
			},
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn to_generic(
 | 
				
			|||
		&self,
 | 
				
			|||
		cnf: &Config,
 | 
				
			|||
		root_certs: &[&str],
 | 
				
			|||
	) -> Result<crate::endpoint::Endpoint, Error> {
 | 
				
			|||
		let mut limits = vec![];
 | 
				
			|||
		for rl_name in self.rate_limits.iter() {
 | 
				
			|||
			let (nb, timeframe) = cnf.get_rate_limit(rl_name)?;
 | 
				
			|||
			limits.push((nb, timeframe));
 | 
				
			|||
		}
 | 
				
			|||
		let mut root_lst: Vec<String> = vec![];
 | 
				
			|||
		root_lst.extend(root_certs.iter().map(|v| v.to_string()));
 | 
				
			|||
		if let Some(crt_lst) = &self.root_certificates {
 | 
				
			|||
			root_lst.extend(crt_lst.iter().map(|v| v.to_owned()));
 | 
				
			|||
		}
 | 
				
			|||
		if let Some(glob) = &cnf.global {
 | 
				
			|||
			if let Some(crt_lst) = &glob.root_certificates {
 | 
				
			|||
				root_lst.extend(crt_lst.iter().map(|v| v.to_owned()));
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		crate::endpoint::Endpoint::new(
 | 
				
			|||
			&self.name,
 | 
				
			|||
			&self.url,
 | 
				
			|||
			self.tos_agreed,
 | 
				
			|||
			&limits,
 | 
				
			|||
			root_lst.as_slice(),
 | 
				
			|||
		)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct RateLimit {
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub number: usize,
 | 
				
			|||
	pub period: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Hook {
 | 
				
			|||
	pub allow_failure: Option<bool>,
 | 
				
			|||
	pub args: Option<Vec<String>>,
 | 
				
			|||
	pub cmd: String,
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub stderr: Option<String>,
 | 
				
			|||
	pub stdin: Option<String>,
 | 
				
			|||
	pub stdin_str: Option<String>,
 | 
				
			|||
	pub stdout: Option<String>,
 | 
				
			|||
	#[serde(rename = "type")]
 | 
				
			|||
	pub hook_type: Vec<HookType>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize)]
 | 
				
			|||
#[serde(rename_all = "kebab-case")]
 | 
				
			|||
pub enum HookType {
 | 
				
			|||
	FilePreCreate,
 | 
				
			|||
	FilePostCreate,
 | 
				
			|||
	FilePreEdit,
 | 
				
			|||
	FilePostEdit,
 | 
				
			|||
	#[serde(rename = "challenge-http-01")]
 | 
				
			|||
	ChallengeHttp01,
 | 
				
			|||
	#[serde(rename = "challenge-http-01-clean")]
 | 
				
			|||
	ChallengeHttp01Clean,
 | 
				
			|||
	#[serde(rename = "challenge-dns-01")]
 | 
				
			|||
	ChallengeDns01,
 | 
				
			|||
	#[serde(rename = "challenge-dns-01-clean")]
 | 
				
			|||
	ChallengeDns01Clean,
 | 
				
			|||
	#[serde(rename = "challenge-tls-alpn-01")]
 | 
				
			|||
	ChallengeTlsAlpn01,
 | 
				
			|||
	#[serde(rename = "challenge-tls-alpn-01-clean")]
 | 
				
			|||
	ChallengeTlsAlpn01Clean,
 | 
				
			|||
	PostOperation,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Group {
 | 
				
			|||
	pub hooks: Vec<String>,
 | 
				
			|||
	pub name: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct ExternalAccount {
 | 
				
			|||
	pub identifier: String,
 | 
				
			|||
	pub key: String,
 | 
				
			|||
	pub signature_algorithm: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ExternalAccount {
 | 
				
			|||
	pub fn to_generic(&self) -> Result<crate::account::ExternalAccount, Error> {
 | 
				
			|||
		let signature_algorithm = match &self.signature_algorithm {
 | 
				
			|||
			Some(a) => a.parse()?,
 | 
				
			|||
			None => crate::DEFAULT_EXTERNAL_ACCOUNT_JWA,
 | 
				
			|||
		};
 | 
				
			|||
		match signature_algorithm {
 | 
				
			|||
			JwsSignatureAlgorithm::Hs256
 | 
				
			|||
			| JwsSignatureAlgorithm::Hs384
 | 
				
			|||
			| JwsSignatureAlgorithm::Hs512 => {}
 | 
				
			|||
			_ => {
 | 
				
			|||
				return Err(format!("{signature_algorithm}: invalid signature algorithm for external account binding").into());
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		Ok(crate::account::ExternalAccount {
 | 
				
			|||
			identifier: self.identifier.to_owned(),
 | 
				
			|||
			key: b64_decode(&self.key)?,
 | 
				
			|||
			signature_algorithm,
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Account {
 | 
				
			|||
	pub contacts: Vec<AccountContact>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
	pub external_account: Option<ExternalAccount>,
 | 
				
			|||
	pub hooks: Option<Vec<String>>,
 | 
				
			|||
	pub key_type: Option<String>,
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub signature_algorithm: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Account {
 | 
				
			|||
	pub fn get_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
 | 
				
			|||
		let lst = match &self.hooks {
 | 
				
			|||
			Some(h) => {
 | 
				
			|||
				let mut res = vec![];
 | 
				
			|||
				for name in h.iter() {
 | 
				
			|||
					let mut h = cnf.get_hook(name)?;
 | 
				
			|||
					res.append(&mut h);
 | 
				
			|||
				}
 | 
				
			|||
				res
 | 
				
			|||
			}
 | 
				
			|||
			None => vec![],
 | 
				
			|||
		};
 | 
				
			|||
		Ok(lst)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn to_generic(
 | 
				
			|||
		&self,
 | 
				
			|||
		file_manager: &FileManager,
 | 
				
			|||
	) -> Result<crate::account::Account, Error> {
 | 
				
			|||
		let contacts: Vec<(String, String)> = self
 | 
				
			|||
			.contacts
 | 
				
			|||
			.iter()
 | 
				
			|||
			.map(|e| (e.get_type(), e.get_value()))
 | 
				
			|||
			.collect();
 | 
				
			|||
		let external_account = match &self.external_account {
 | 
				
			|||
			Some(a) => Some(a.to_generic()?),
 | 
				
			|||
			None => None,
 | 
				
			|||
		};
 | 
				
			|||
		crate::account::Account::load(
 | 
				
			|||
			file_manager,
 | 
				
			|||
			&self.name,
 | 
				
			|||
			&contacts,
 | 
				
			|||
			&self.key_type,
 | 
				
			|||
			&self.signature_algorithm,
 | 
				
			|||
			&external_account,
 | 
				
			|||
		)
 | 
				
			|||
		.await
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct AccountContact {
 | 
				
			|||
	pub mailto: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl AccountContact {
 | 
				
			|||
	pub fn get_type(&self) -> String {
 | 
				
			|||
		"mailto".to_string()
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_value(&self) -> String {
 | 
				
			|||
		self.mailto.clone()
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Certificate {
 | 
				
			|||
	pub account: String,
 | 
				
			|||
	pub csr_digest: Option<String>,
 | 
				
			|||
	pub directory: Option<String>,
 | 
				
			|||
	pub endpoint: String,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
	pub file_name_format: Option<String>,
 | 
				
			|||
	pub hooks: Vec<String>,
 | 
				
			|||
	pub identifiers: Vec<Identifier>,
 | 
				
			|||
	pub key_type: Option<String>,
 | 
				
			|||
	pub kp_reuse: Option<bool>,
 | 
				
			|||
	pub name: Option<String>,
 | 
				
			|||
	pub random_early_renew: Option<String>,
 | 
				
			|||
	pub renew_delay: Option<String>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub subject_attributes: SubjectAttributes,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Certificate {
 | 
				
			|||
	pub fn get_key_type(&self) -> Result<KeyType, Error> {
 | 
				
			|||
		match &self.key_type {
 | 
				
			|||
			Some(a) => a.parse(),
 | 
				
			|||
			None => Ok(crate::DEFAULT_CERT_KEY_TYPE),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_csr_digest(&self) -> Result<HashFunction, Error> {
 | 
				
			|||
		match &self.csr_digest {
 | 
				
			|||
			Some(d) => d.parse(),
 | 
				
			|||
			None => Ok(crate::DEFAULT_CSR_DIGEST),
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_identifiers(&self) -> Result<Vec<crate::identifier::Identifier>, Error> {
 | 
				
			|||
		let mut ret = vec![];
 | 
				
			|||
		for id in self.identifiers.iter() {
 | 
				
			|||
			ret.push(id.to_generic()?);
 | 
				
			|||
		}
 | 
				
			|||
		Ok(ret)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_kp_reuse(&self) -> bool {
 | 
				
			|||
		match self.kp_reuse {
 | 
				
			|||
			Some(b) => b,
 | 
				
			|||
			None => crate::DEFAULT_KP_REUSE,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_crt_name(&self) -> Result<String, Error> {
 | 
				
			|||
		let name = match &self.name {
 | 
				
			|||
			Some(n) => n.to_string(),
 | 
				
			|||
			None => {
 | 
				
			|||
				let id = self
 | 
				
			|||
					.identifiers
 | 
				
			|||
					.first()
 | 
				
			|||
					.ok_or_else(|| Error::from("certificate has no identifiers"))?;
 | 
				
			|||
				id.to_string()
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		let name = name.replace(['*', ':', '/'], "_");
 | 
				
			|||
		Ok(name)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_crt_name_format(&self, cnf: &Config) -> Result<String, Error> {
 | 
				
			|||
		match &self.file_name_format {
 | 
				
			|||
			Some(n) => Ok(n.to_string()),
 | 
				
			|||
			None => {
 | 
				
			|||
				let ep = self.do_get_endpoint(cnf)?;
 | 
				
			|||
				Ok(ep.get_crt_name_format(cnf))
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_crt_dir(&self, cnf: &Config) -> String {
 | 
				
			|||
		let crt_directory = match &self.directory {
 | 
				
			|||
			Some(d) => d,
 | 
				
			|||
			None => match &cnf.global {
 | 
				
			|||
				Some(g) => match &g.certificates_directory {
 | 
				
			|||
					Some(d) => d,
 | 
				
			|||
					None => crate::DEFAULT_CERT_DIR,
 | 
				
			|||
				},
 | 
				
			|||
				None => crate::DEFAULT_CERT_DIR,
 | 
				
			|||
			},
 | 
				
			|||
		};
 | 
				
			|||
		crt_directory.to_string()
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn do_get_endpoint(&self, cnf: &Config) -> Result<Endpoint, Error> {
 | 
				
			|||
		for endpoint in cnf.endpoint.iter() {
 | 
				
			|||
			if endpoint.name == self.endpoint {
 | 
				
			|||
				return Ok(endpoint.clone());
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		Err(format!("{}: unknown endpoint", self.endpoint).into())
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_endpoint(
 | 
				
			|||
		&self,
 | 
				
			|||
		cnf: &Config,
 | 
				
			|||
		root_certs: &[&str],
 | 
				
			|||
	) -> Result<crate::endpoint::Endpoint, Error> {
 | 
				
			|||
		let endpoint = self.do_get_endpoint(cnf)?;
 | 
				
			|||
		endpoint.to_generic(cnf, root_certs)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
 | 
				
			|||
		let mut res = vec![];
 | 
				
			|||
		for name in self.hooks.iter() {
 | 
				
			|||
			let mut h = cnf.get_hook(name)?;
 | 
				
			|||
			res.append(&mut h);
 | 
				
			|||
		}
 | 
				
			|||
		Ok(res)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_random_early_renew(&self, cnf: &Config) -> Result<Duration, Error> {
 | 
				
			|||
		match &self.random_early_renew {
 | 
				
			|||
			Some(d) => parse_duration(d),
 | 
				
			|||
			None => {
 | 
				
			|||
				let endpoint = self.do_get_endpoint(cnf)?;
 | 
				
			|||
				endpoint.get_random_early_renew(cnf)
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_renew_delay(&self, cnf: &Config) -> Result<Duration, Error> {
 | 
				
			|||
		match &self.renew_delay {
 | 
				
			|||
			Some(d) => parse_duration(d),
 | 
				
			|||
			None => {
 | 
				
			|||
				let endpoint = self.do_get_endpoint(cnf)?;
 | 
				
			|||
				endpoint.get_renew_delay(cnf)
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize)]
 | 
				
			|||
#[serde(remote = "Self")]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct Identifier {
 | 
				
			|||
	pub challenge: String,
 | 
				
			|||
	pub dns: Option<String>,
 | 
				
			|||
	#[serde(default)]
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
	pub ip: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl<'de> Deserialize<'de> for Identifier {
 | 
				
			|||
	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 | 
				
			|||
	where
 | 
				
			|||
		D: Deserializer<'de>,
 | 
				
			|||
	{
 | 
				
			|||
		let unchecked = Identifier::deserialize(deserializer)?;
 | 
				
			|||
		let filled_nb: u8 = [unchecked.dns.is_some(), unchecked.ip.is_some()]
 | 
				
			|||
			.iter()
 | 
				
			|||
			.copied()
 | 
				
			|||
			.map(u8::from)
 | 
				
			|||
			.sum();
 | 
				
			|||
		if filled_nb != 1 {
 | 
				
			|||
			return Err(de::Error::custom(
 | 
				
			|||
				"one and only one of `dns` or `ip` must be specified",
 | 
				
			|||
			));
 | 
				
			|||
		}
 | 
				
			|||
		Ok(unchecked)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Identifier {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = String::new();
 | 
				
			|||
		let msg = self.dns.as_ref().or(self.ip.as_ref()).unwrap_or(&s);
 | 
				
			|||
		write!(f, "{msg}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Identifier {
 | 
				
			|||
	fn to_generic(&self) -> Result<crate::identifier::Identifier, Error> {
 | 
				
			|||
		let (t, v) = match &self.dns {
 | 
				
			|||
			Some(d) => (IdentifierType::Dns, d),
 | 
				
			|||
			None => match &self.ip {
 | 
				
			|||
				Some(ip) => (IdentifierType::Ip, ip),
 | 
				
			|||
				None => {
 | 
				
			|||
					return Err("no identifier found".into());
 | 
				
			|||
				}
 | 
				
			|||
			},
 | 
				
			|||
		};
 | 
				
			|||
		crate::identifier::Identifier::new(t, v, &self.challenge, &self.env)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Default, Deserialize)]
 | 
				
			|||
#[serde(deny_unknown_fields)]
 | 
				
			|||
pub struct SubjectAttributes {
 | 
				
			|||
	pub country_name: Option<String>,
 | 
				
			|||
	pub generation_qualifier: Option<String>,
 | 
				
			|||
	pub given_name: Option<String>,
 | 
				
			|||
	pub initials: Option<String>,
 | 
				
			|||
	pub locality_name: Option<String>,
 | 
				
			|||
	pub name: Option<String>,
 | 
				
			|||
	pub organization_name: Option<String>,
 | 
				
			|||
	pub organizational_unit_name: Option<String>,
 | 
				
			|||
	pub pkcs9_email_address: Option<String>,
 | 
				
			|||
	pub postal_address: Option<String>,
 | 
				
			|||
	pub postal_code: Option<String>,
 | 
				
			|||
	pub state_or_province_name: Option<String>,
 | 
				
			|||
	pub street: Option<String>,
 | 
				
			|||
	pub surname: Option<String>,
 | 
				
			|||
	pub title: Option<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl SubjectAttributes {
 | 
				
			|||
	pub fn to_generic(&self) -> HashMap<SubjectAttribute, String> {
 | 
				
			|||
		let mut ret = HashMap::new();
 | 
				
			|||
		push_subject_attr!(ret, self.country_name, CountryName);
 | 
				
			|||
		push_subject_attr!(ret, self.generation_qualifier, GenerationQualifier);
 | 
				
			|||
		push_subject_attr!(ret, self.given_name, GivenName);
 | 
				
			|||
		push_subject_attr!(ret, self.initials, Initials);
 | 
				
			|||
		push_subject_attr!(ret, self.locality_name, LocalityName);
 | 
				
			|||
		push_subject_attr!(ret, self.name, Name);
 | 
				
			|||
		push_subject_attr!(ret, self.organization_name, OrganizationName);
 | 
				
			|||
		push_subject_attr!(ret, self.organizational_unit_name, OrganizationalUnitName);
 | 
				
			|||
		push_subject_attr!(ret, self.pkcs9_email_address, Pkcs9EmailAddress);
 | 
				
			|||
		push_subject_attr!(ret, self.postal_address, PostalAddress);
 | 
				
			|||
		push_subject_attr!(ret, self.postal_code, PostalCode);
 | 
				
			|||
		push_subject_attr!(ret, self.state_or_province_name, StateOrProvinceName);
 | 
				
			|||
		push_subject_attr!(ret, self.street, Street);
 | 
				
			|||
		push_subject_attr!(ret, self.surname, Surname);
 | 
				
			|||
		push_subject_attr!(ret, self.title, Title);
 | 
				
			|||
		ret
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn create_dir(path: &str) -> Result<(), Error> {
 | 
				
			|||
	if Path::new(path).is_dir() {
 | 
				
			|||
		Ok(())
 | 
				
			|||
	} else {
 | 
				
			|||
		fs::create_dir_all(path)?;
 | 
				
			|||
		Ok(())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn init_directories(config: &Config) -> Result<(), Error> {
 | 
				
			|||
	create_dir(&config.get_account_dir())?;
 | 
				
			|||
	for crt in config.certificate.iter() {
 | 
				
			|||
		create_dir(&crt.get_crt_dir(config))?;
 | 
				
			|||
	}
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_cnf_path(from: &Path, file: &str) -> Result<Vec<PathBuf>, Error> {
 | 
				
			|||
	let mut path = from.to_path_buf().canonicalize()?;
 | 
				
			|||
	path.pop();
 | 
				
			|||
	path.push(file);
 | 
				
			|||
	let err = format!("{path:?}: invalid UTF-8 path");
 | 
				
			|||
	let raw_path = path.to_str().ok_or(err)?;
 | 
				
			|||
	let g = glob(raw_path)?
 | 
				
			|||
		.filter_map(Result::ok)
 | 
				
			|||
		.collect::<Vec<PathBuf>>();
 | 
				
			|||
	if g.is_empty() {
 | 
				
			|||
		log::warn!(
 | 
				
			|||
			"pattern `{file}` (expanded as `{raw_path}`): no matching configuration file found"
 | 
				
			|||
		);
 | 
				
			|||
	}
 | 
				
			|||
	Ok(g)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn read_cnf(path: &Path, loaded_files: &mut BTreeSet<PathBuf>) -> Result<Config, Error> {
 | 
				
			|||
	let path = path
 | 
				
			|||
		.canonicalize()
 | 
				
			|||
		.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
 | 
				
			|||
	if loaded_files.contains(&path) {
 | 
				
			|||
		info!("{}: configuration file already loaded", path.display());
 | 
				
			|||
		return Ok(Config::default());
 | 
				
			|||
	}
 | 
				
			|||
	loaded_files.insert(path.clone());
 | 
				
			|||
	info!("{}: loading configuration file", &path.display());
 | 
				
			|||
	let mut file =
 | 
				
			|||
		File::open(&path).map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
 | 
				
			|||
	let mut contents = String::new();
 | 
				
			|||
	file.read_to_string(&mut contents)
 | 
				
			|||
		.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
 | 
				
			|||
	let mut config: Config = toml::from_str(&contents)
 | 
				
			|||
		.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
 | 
				
			|||
	for cnf_name in config.include.iter() {
 | 
				
			|||
		for cnf_path in get_cnf_path(&path, cnf_name)? {
 | 
				
			|||
			let mut add_cnf = read_cnf(&cnf_path, loaded_files)?;
 | 
				
			|||
			config.endpoint.append(&mut add_cnf.endpoint);
 | 
				
			|||
			config.rate_limit.append(&mut add_cnf.rate_limit);
 | 
				
			|||
			config.hook.append(&mut add_cnf.hook);
 | 
				
			|||
			config.group.append(&mut add_cnf.group);
 | 
				
			|||
			config.account.append(&mut add_cnf.account);
 | 
				
			|||
			config.certificate.append(&mut add_cnf.certificate);
 | 
				
			|||
			if config.global.is_none() {
 | 
				
			|||
				config.global = add_cnf.global;
 | 
				
			|||
			} else if let Some(new_glob) = add_cnf.global {
 | 
				
			|||
				let mut tmp_glob = config.global.clone().unwrap();
 | 
				
			|||
				set_cfg_attr!(tmp_glob.accounts_directory, new_glob.accounts_directory);
 | 
				
			|||
				set_cfg_attr!(
 | 
				
			|||
					tmp_glob.certificates_directory,
 | 
				
			|||
					new_glob.certificates_directory
 | 
				
			|||
				);
 | 
				
			|||
				set_cfg_attr!(tmp_glob.cert_file_mode, new_glob.cert_file_mode);
 | 
				
			|||
				set_cfg_attr!(tmp_glob.cert_file_user, new_glob.cert_file_user);
 | 
				
			|||
				set_cfg_attr!(tmp_glob.cert_file_group, new_glob.cert_file_group);
 | 
				
			|||
				set_cfg_attr!(tmp_glob.pk_file_mode, new_glob.pk_file_mode);
 | 
				
			|||
				set_cfg_attr!(tmp_glob.pk_file_user, new_glob.pk_file_user);
 | 
				
			|||
				set_cfg_attr!(tmp_glob.pk_file_group, new_glob.pk_file_group);
 | 
				
			|||
				config.global = Some(tmp_glob);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
	Ok(config)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn dispatch_global_env_vars(config: &mut Config) {
 | 
				
			|||
	if let Some(glob) = &config.global {
 | 
				
			|||
		if !glob.env.is_empty() {
 | 
				
			|||
			for cert in config.certificate.iter_mut() {
 | 
				
			|||
				let mut new_vars = glob.env.clone();
 | 
				
			|||
				for (k, v) in cert.env.iter() {
 | 
				
			|||
					new_vars.insert(k.to_string(), v.to_string());
 | 
				
			|||
				}
 | 
				
			|||
				cert.env = new_vars;
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn from_file(file_name: &str) -> Result<Config, Error> {
 | 
				
			|||
	let path = PathBuf::from(file_name);
 | 
				
			|||
	let mut loaded_files = BTreeSet::new();
 | 
				
			|||
	let mut config = read_cnf(&path, &mut loaded_files)?;
 | 
				
			|||
	dispatch_global_env_vars(&mut config);
 | 
				
			|||
	init_directories(&config)?;
 | 
				
			|||
	Ok(config)
 | 
				
			|||
}
 | 
				
			|||
@ -1,51 +0,0 @@ | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use nom::bytes::complete::take_while_m_n;
 | 
				
			|||
use nom::character::complete::digit1;
 | 
				
			|||
use nom::combinator::map_res;
 | 
				
			|||
use nom::multi::fold_many1;
 | 
				
			|||
use nom::IResult;
 | 
				
			|||
use std::time::Duration;
 | 
				
			|||
 | 
				
			|||
fn is_duration_chr(c: char) -> bool {
 | 
				
			|||
	c == 's' || c == 'm' || c == 'h' || c == 'd' || c == 'w'
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_multiplicator(input: &str) -> IResult<&str, u64> {
 | 
				
			|||
	let (input, nb) = take_while_m_n(1, 1, is_duration_chr)(input)?;
 | 
				
			|||
	let mult = match nb.chars().next() {
 | 
				
			|||
		Some('s') => 1,
 | 
				
			|||
		Some('m') => 60,
 | 
				
			|||
		Some('h') => 3_600,
 | 
				
			|||
		Some('d') => 86_400,
 | 
				
			|||
		Some('w') => 604_800,
 | 
				
			|||
		_ => 0,
 | 
				
			|||
	};
 | 
				
			|||
	Ok((input, mult))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_duration_part(input: &str) -> IResult<&str, Duration> {
 | 
				
			|||
	let (input, nb) = map_res(digit1, |s: &str| s.parse::<u64>())(input)?;
 | 
				
			|||
	let (input, mult) = get_multiplicator(input)?;
 | 
				
			|||
	Ok((input, Duration::from_secs(nb * mult)))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_duration(input: &str) -> IResult<&str, Duration> {
 | 
				
			|||
	fold_many1(
 | 
				
			|||
		get_duration_part,
 | 
				
			|||
		|| Duration::new(0, 0),
 | 
				
			|||
		|mut acc: Duration, item| {
 | 
				
			|||
			acc += item;
 | 
				
			|||
			acc
 | 
				
			|||
		},
 | 
				
			|||
	)(input)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn parse_duration(input: &str) -> Result<Duration, Error> {
 | 
				
			|||
	match get_duration(input) {
 | 
				
			|||
		Ok((r, d)) => match r.len() {
 | 
				
			|||
			0 => Ok(d),
 | 
				
			|||
			_ => Err(format!("{input}: invalid duration").into()),
 | 
				
			|||
		},
 | 
				
			|||
		Err(_) => Err(format!("{input}: invalid duration").into()),
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,130 +0,0 @@ | 
				
			|||
use crate::acme_proto::structs::Directory;
 | 
				
			|||
use crate::duration::parse_duration;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use std::cmp;
 | 
				
			|||
use std::time::{Duration, Instant};
 | 
				
			|||
use tokio::time::sleep;
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct Endpoint {
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub url: String,
 | 
				
			|||
	pub tos_agreed: bool,
 | 
				
			|||
	pub nonce: Option<String>,
 | 
				
			|||
	pub rl: RateLimit,
 | 
				
			|||
	pub dir: Directory,
 | 
				
			|||
	pub root_certificates: Vec<String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Endpoint {
 | 
				
			|||
	pub fn new(
 | 
				
			|||
		name: &str,
 | 
				
			|||
		url: &str,
 | 
				
			|||
		tos_agreed: bool,
 | 
				
			|||
		limits: &[(usize, String)],
 | 
				
			|||
		root_certs: &[String],
 | 
				
			|||
	) -> Result<Self, Error> {
 | 
				
			|||
		Ok(Self {
 | 
				
			|||
			name: name.to_string(),
 | 
				
			|||
			url: url.to_string(),
 | 
				
			|||
			tos_agreed,
 | 
				
			|||
			nonce: None,
 | 
				
			|||
			rl: RateLimit::new(limits)?,
 | 
				
			|||
			dir: Directory {
 | 
				
			|||
				meta: None,
 | 
				
			|||
				new_nonce: String::new(),
 | 
				
			|||
				new_account: String::new(),
 | 
				
			|||
				new_order: String::new(),
 | 
				
			|||
				new_authz: None,
 | 
				
			|||
				revoke_cert: String::new(),
 | 
				
			|||
				key_change: String::new(),
 | 
				
			|||
			},
 | 
				
			|||
			root_certificates: root_certs.to_vec(),
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct RateLimit {
 | 
				
			|||
	limits: Vec<(usize, Duration)>,
 | 
				
			|||
	query_log: Vec<Instant>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl RateLimit {
 | 
				
			|||
	pub fn new(raw_limits: &[(usize, String)]) -> Result<Self, Error> {
 | 
				
			|||
		let mut limits = vec![];
 | 
				
			|||
		for (nb, raw_duration) in raw_limits.iter() {
 | 
				
			|||
			let parsed_duration = parse_duration(raw_duration)?;
 | 
				
			|||
			limits.push((*nb, parsed_duration));
 | 
				
			|||
		}
 | 
				
			|||
		limits.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
 | 
				
			|||
		limits.reverse();
 | 
				
			|||
		Ok(Self {
 | 
				
			|||
			limits,
 | 
				
			|||
			query_log: vec![],
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn block_until_allowed(&mut self) {
 | 
				
			|||
		if self.limits.is_empty() {
 | 
				
			|||
			return;
 | 
				
			|||
		}
 | 
				
			|||
		let mut sleep_duration = self.get_sleep_duration();
 | 
				
			|||
		loop {
 | 
				
			|||
			sleep(sleep_duration).await;
 | 
				
			|||
			self.prune_log();
 | 
				
			|||
			if self.request_allowed() {
 | 
				
			|||
				self.query_log.push(Instant::now());
 | 
				
			|||
				return;
 | 
				
			|||
			}
 | 
				
			|||
			sleep_duration = self.get_sleep_duration();
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn get_sleep_duration(&self) -> Duration {
 | 
				
			|||
		let (nb_req, min_duration) = match self.limits.last() {
 | 
				
			|||
			Some((n, d)) => (*n as u64, *d),
 | 
				
			|||
			None => {
 | 
				
			|||
				return Duration::from_millis(0);
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		let nb_mili = match min_duration.as_secs() {
 | 
				
			|||
			0 | 1 => crate::MIN_RATE_LIMIT_SLEEP_MILISEC,
 | 
				
			|||
			n => {
 | 
				
			|||
				let a = n * 200 / nb_req;
 | 
				
			|||
				let a = cmp::min(a, crate::MAX_RATE_LIMIT_SLEEP_MILISEC);
 | 
				
			|||
				cmp::max(a, crate::MIN_RATE_LIMIT_SLEEP_MILISEC)
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		Duration::from_millis(nb_mili)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn request_allowed(&self) -> bool {
 | 
				
			|||
		for (max_allowed, duration) in self.limits.iter() {
 | 
				
			|||
			match Instant::now().checked_sub(*duration) {
 | 
				
			|||
				Some(max_date) => {
 | 
				
			|||
					let nb_req = self
 | 
				
			|||
						.query_log
 | 
				
			|||
						.iter()
 | 
				
			|||
						.filter(move |x| **x > max_date)
 | 
				
			|||
						.count();
 | 
				
			|||
					if nb_req >= *max_allowed {
 | 
				
			|||
						return false;
 | 
				
			|||
					}
 | 
				
			|||
				}
 | 
				
			|||
				None => {
 | 
				
			|||
					return false;
 | 
				
			|||
				}
 | 
				
			|||
			};
 | 
				
			|||
		}
 | 
				
			|||
		true
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn prune_log(&mut self) {
 | 
				
			|||
		if let Some((_, max_limit)) = self.limits.first() {
 | 
				
			|||
			if let Some(prune_date) = Instant::now().checked_sub(*max_limit) {
 | 
				
			|||
				self.query_log.retain(move |&d| d > prune_date);
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,215 +0,0 @@ | 
				
			|||
pub use crate::config::HookType;
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::template::render_template;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use async_process::{Command, Stdio};
 | 
				
			|||
use futures::AsyncWriteExt;
 | 
				
			|||
use serde::Serialize;
 | 
				
			|||
use std::collections::hash_map::Iter;
 | 
				
			|||
use std::collections::{HashMap, HashSet};
 | 
				
			|||
use std::fs::File;
 | 
				
			|||
use std::io::prelude::*;
 | 
				
			|||
use std::io::BufReader;
 | 
				
			|||
use std::path::PathBuf;
 | 
				
			|||
use std::{env, fmt};
 | 
				
			|||
 | 
				
			|||
pub trait HookEnvData {
 | 
				
			|||
	fn set_env(&mut self, env: &HashMap<String, String>);
 | 
				
			|||
	fn get_env(&self) -> Iter<String, String>;
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn deref<F, G>(t: (&F, &G)) -> (F, G)
 | 
				
			|||
where
 | 
				
			|||
	F: Clone,
 | 
				
			|||
	G: Clone,
 | 
				
			|||
{
 | 
				
			|||
	((*(t.0)).to_owned(), (*(t.1)).to_owned())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! imple_hook_data_env {
 | 
				
			|||
	($t: ty) => {
 | 
				
			|||
		impl HookEnvData for $t {
 | 
				
			|||
			fn set_env(&mut self, env: &HashMap<String, String>) {
 | 
				
			|||
				for (key, value) in env::vars().chain(env.iter().map(deref)) {
 | 
				
			|||
					self.env.insert(key, value);
 | 
				
			|||
				}
 | 
				
			|||
			}
 | 
				
			|||
 | 
				
			|||
			fn get_env(&self) -> Iter<String, String> {
 | 
				
			|||
				self.env.iter()
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Serialize)]
 | 
				
			|||
pub struct PostOperationHookData {
 | 
				
			|||
	pub identifiers: Vec<String>,
 | 
				
			|||
	pub key_type: String,
 | 
				
			|||
	pub status: String,
 | 
				
			|||
	pub is_success: bool,
 | 
				
			|||
	pub certificate_path: PathBuf,
 | 
				
			|||
	pub private_key_path: PathBuf,
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
imple_hook_data_env!(PostOperationHookData);
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Serialize)]
 | 
				
			|||
pub struct ChallengeHookData {
 | 
				
			|||
	pub identifier: String,
 | 
				
			|||
	pub identifier_tls_alpn: String,
 | 
				
			|||
	pub challenge: String,
 | 
				
			|||
	pub file_name: String,
 | 
				
			|||
	pub proof: String,
 | 
				
			|||
	pub raw_proof: String,
 | 
				
			|||
	pub is_clean_hook: bool,
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
imple_hook_data_env!(ChallengeHookData);
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Serialize)]
 | 
				
			|||
pub struct FileStorageHookData {
 | 
				
			|||
	// TODO: add the current operation (create/edit)
 | 
				
			|||
	pub file_name: String,
 | 
				
			|||
	pub file_directory: String,
 | 
				
			|||
	pub file_path: PathBuf,
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
imple_hook_data_env!(FileStorageHookData);
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub enum HookStdin {
 | 
				
			|||
	File(String),
 | 
				
			|||
	Str(String),
 | 
				
			|||
	None,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct Hook {
 | 
				
			|||
	pub name: String,
 | 
				
			|||
	pub hook_type: HashSet<HookType>,
 | 
				
			|||
	pub cmd: String,
 | 
				
			|||
	pub args: Option<Vec<String>>,
 | 
				
			|||
	pub stdin: HookStdin,
 | 
				
			|||
	pub stdout: Option<String>,
 | 
				
			|||
	pub stderr: Option<String>,
 | 
				
			|||
	pub allow_failure: bool,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Hook {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		write!(f, "{}", self.name)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! get_hook_output {
 | 
				
			|||
	($logger: expr, $out: expr, $data: expr, $hook_name: expr, $out_name: expr) => {{
 | 
				
			|||
		match $out {
 | 
				
			|||
			Some(path) => {
 | 
				
			|||
				let path = render_template(path, $data)?;
 | 
				
			|||
				$logger.trace(&format!("hook \"{}\": {}: {path}", $hook_name, $out_name));
 | 
				
			|||
				let file = File::create(&path)?;
 | 
				
			|||
				Stdio::from(file)
 | 
				
			|||
			}
 | 
				
			|||
			None => Stdio::null(),
 | 
				
			|||
		}
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn call_single<L, T>(logger: &L, data: &T, hook: &Hook) -> Result<(), Error>
 | 
				
			|||
where
 | 
				
			|||
	L: HasLogger,
 | 
				
			|||
	T: Clone + HookEnvData + Serialize,
 | 
				
			|||
{
 | 
				
			|||
	logger.debug(&format!("calling hook \"{}\"", hook.name));
 | 
				
			|||
	let mut v = vec![];
 | 
				
			|||
	let args = match &hook.args {
 | 
				
			|||
		Some(lst) => {
 | 
				
			|||
			for fmt in lst.iter() {
 | 
				
			|||
				let s = render_template(fmt, &data)?;
 | 
				
			|||
				v.push(s);
 | 
				
			|||
			}
 | 
				
			|||
			v.as_slice()
 | 
				
			|||
		}
 | 
				
			|||
		None => &[],
 | 
				
			|||
	};
 | 
				
			|||
	logger.trace(&format!("hook \"{}\": cmd: {}", hook.name, hook.cmd));
 | 
				
			|||
	logger.trace(&format!("hook \"{}\": args: {args:?}", hook.name));
 | 
				
			|||
	let mut cmd = Command::new(&hook.cmd)
 | 
				
			|||
		.envs(data.get_env())
 | 
				
			|||
		.args(args)
 | 
				
			|||
		.stdout(get_hook_output!(
 | 
				
			|||
			logger,
 | 
				
			|||
			&hook.stdout,
 | 
				
			|||
			&data,
 | 
				
			|||
			&hook.name,
 | 
				
			|||
			"stdout"
 | 
				
			|||
		))
 | 
				
			|||
		.stderr(get_hook_output!(
 | 
				
			|||
			logger,
 | 
				
			|||
			&hook.stderr,
 | 
				
			|||
			&data,
 | 
				
			|||
			&hook.name,
 | 
				
			|||
			"stderr"
 | 
				
			|||
		))
 | 
				
			|||
		.stdin(match &hook.stdin {
 | 
				
			|||
			HookStdin::Str(_) | HookStdin::File(_) => Stdio::piped(),
 | 
				
			|||
			HookStdin::None => Stdio::null(),
 | 
				
			|||
		})
 | 
				
			|||
		.spawn()?;
 | 
				
			|||
	match &hook.stdin {
 | 
				
			|||
		HookStdin::Str(s) => {
 | 
				
			|||
			let data_in = render_template(s, &data)?;
 | 
				
			|||
			logger.trace(&format!("hook \"{}\": string stdin: {data_in}", hook.name));
 | 
				
			|||
			let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?;
 | 
				
			|||
			stdin.write_all(data_in.as_bytes()).await?;
 | 
				
			|||
		}
 | 
				
			|||
		HookStdin::File(f) => {
 | 
				
			|||
			let file_name = render_template(f, &data)?;
 | 
				
			|||
			logger.trace(&format!("hook \"{}\": file stdin: {file_name}", hook.name));
 | 
				
			|||
			let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?;
 | 
				
			|||
			let file = File::open(&file_name).map_err(|e| Error::from(e).prefix(&file_name))?;
 | 
				
			|||
			let buf_reader = BufReader::new(file);
 | 
				
			|||
			for line in buf_reader.lines() {
 | 
				
			|||
				let line = format!("{}\n", line?);
 | 
				
			|||
				stdin.write_all(line.as_bytes()).await?;
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		HookStdin::None => {}
 | 
				
			|||
	}
 | 
				
			|||
	// TODO: add a timeout
 | 
				
			|||
	let status = cmd.status().await?;
 | 
				
			|||
	if !status.success() && !hook.allow_failure {
 | 
				
			|||
		let msg = match status.code() {
 | 
				
			|||
			Some(code) => format!("unrecoverable failure: code {code}").into(),
 | 
				
			|||
			None => "unrecoverable failure".into(),
 | 
				
			|||
		};
 | 
				
			|||
		return Err(msg);
 | 
				
			|||
	}
 | 
				
			|||
	match status.code() {
 | 
				
			|||
		Some(code) => logger.debug(&format!("hook \"{}\": exited: code {code}", hook.name)),
 | 
				
			|||
		None => logger.debug(&format!("hook \"{}\": exited", hook.name)),
 | 
				
			|||
	};
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn call<L, T>(
 | 
				
			|||
	logger: &L,
 | 
				
			|||
	hooks: &[Hook],
 | 
				
			|||
	data: &T,
 | 
				
			|||
	hook_type: HookType,
 | 
				
			|||
) -> Result<(), Error>
 | 
				
			|||
where
 | 
				
			|||
	L: HasLogger,
 | 
				
			|||
	T: Clone + HookEnvData + Serialize,
 | 
				
			|||
{
 | 
				
			|||
	for hook in hooks.iter().filter(|h| h.hook_type.contains(&hook_type)) {
 | 
				
			|||
		call_single(logger, data, hook)
 | 
				
			|||
			.await
 | 
				
			|||
			.map_err(|e| e.prefix(&hook.name))?;
 | 
				
			|||
	}
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
@ -1,288 +0,0 @@ | 
				
			|||
use crate::acme_proto::structs::{AcmeError, HttpApiError};
 | 
				
			|||
use crate::endpoint::Endpoint;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use reqwest::header::{HeaderMap, HeaderValue};
 | 
				
			|||
use reqwest::{header, Client, ClientBuilder, Response};
 | 
				
			|||
use std::fs::File;
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
use std::io::prelude::*;
 | 
				
			|||
use std::{thread, time};
 | 
				
			|||
 | 
				
			|||
pub const CONTENT_TYPE_JOSE: &str = "application/jose+json";
 | 
				
			|||
pub const CONTENT_TYPE_JSON: &str = "application/json";
 | 
				
			|||
pub const CONTENT_TYPE_PEM: &str = "application/pem-certificate-chain";
 | 
				
			|||
pub const HEADER_NONCE: &str = "Replay-Nonce";
 | 
				
			|||
pub const HEADER_LOCATION: &str = "Location";
 | 
				
			|||
 | 
				
			|||
pub struct ValidHttpResponse {
 | 
				
			|||
	headers: HeaderMap,
 | 
				
			|||
	pub body: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl ValidHttpResponse {
 | 
				
			|||
	pub fn get_header(&self, name: &str) -> Option<String> {
 | 
				
			|||
		match self.headers.get(name) {
 | 
				
			|||
			Some(r) => match header_to_string(r) {
 | 
				
			|||
				Ok(h) => Some(h),
 | 
				
			|||
				Err(_) => None,
 | 
				
			|||
			},
 | 
				
			|||
			None => None,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn json<T>(&self) -> Result<T, Error>
 | 
				
			|||
	where
 | 
				
			|||
		T: serde::de::DeserializeOwned,
 | 
				
			|||
	{
 | 
				
			|||
		serde_json::from_str(&self.body).map_err(Error::from)
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	async fn from_response(response: Response) -> Result<Self, Error> {
 | 
				
			|||
		let headers = response.headers().clone();
 | 
				
			|||
		let body = response.text().await?;
 | 
				
			|||
		log::trace!("HTTP response headers: {headers:?}");
 | 
				
			|||
		log::trace!("HTTP response body: {body}");
 | 
				
			|||
		Ok(ValidHttpResponse { headers, body })
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub enum HttpError {
 | 
				
			|||
	ApiError(HttpApiError),
 | 
				
			|||
	GenericError(Error),
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl HttpError {
 | 
				
			|||
	pub fn in_err(error: HttpError) -> Error {
 | 
				
			|||
		match error {
 | 
				
			|||
			HttpError::ApiError(e) => e.to_string().into(),
 | 
				
			|||
			HttpError::GenericError(e) => e,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn is_acme_err(&self, acme_error: AcmeError) -> bool {
 | 
				
			|||
		match self {
 | 
				
			|||
			HttpError::ApiError(aerr) => aerr.get_acme_type() == acme_error,
 | 
				
			|||
			HttpError::GenericError(_) => false,
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<Error> for HttpError {
 | 
				
			|||
	fn from(error: Error) -> Self {
 | 
				
			|||
		HttpError::GenericError(error)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<HttpApiError> for HttpError {
 | 
				
			|||
	fn from(error: HttpApiError) -> Self {
 | 
				
			|||
		HttpError::ApiError(error)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<&str> for HttpError {
 | 
				
			|||
	fn from(error: &str) -> Self {
 | 
				
			|||
		HttpError::GenericError(error.into())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<String> for HttpError {
 | 
				
			|||
	fn from(error: String) -> Self {
 | 
				
			|||
		HttpError::GenericError(error.into())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl From<reqwest::Error> for HttpError {
 | 
				
			|||
	fn from(error: reqwest::Error) -> Self {
 | 
				
			|||
		HttpError::GenericError(error.into())
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn is_nonce(data: &str) -> bool {
 | 
				
			|||
	!data.is_empty()
 | 
				
			|||
		&& data
 | 
				
			|||
			.bytes()
 | 
				
			|||
			.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn new_nonce(endpoint: &mut Endpoint) -> Result<(), HttpError> {
 | 
				
			|||
	rate_limit(endpoint).await;
 | 
				
			|||
	let url = endpoint.dir.new_nonce.clone();
 | 
				
			|||
	let _ = get(endpoint, &url).await?;
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn update_nonce(endpoint: &mut Endpoint, response: &Response) -> Result<(), Error> {
 | 
				
			|||
	if let Some(nonce) = response.headers().get(HEADER_NONCE) {
 | 
				
			|||
		let nonce = header_to_string(nonce)?;
 | 
				
			|||
		if !is_nonce(&nonce) {
 | 
				
			|||
			let msg = format!("{nonce}: invalid nonce.");
 | 
				
			|||
			return Err(msg.into());
 | 
				
			|||
		}
 | 
				
			|||
		endpoint.nonce = Some(nonce);
 | 
				
			|||
	}
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn check_status(response: &Response) -> Result<(), Error> {
 | 
				
			|||
	if !response.status().is_success() {
 | 
				
			|||
		let status = response.status();
 | 
				
			|||
		let msg = format!("HTTP error: {}: {}", status.as_u16(), status.as_str());
 | 
				
			|||
		return Err(msg.into());
 | 
				
			|||
	}
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn rate_limit(endpoint: &mut Endpoint) {
 | 
				
			|||
	endpoint.rl.block_until_allowed().await;
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn header_to_string(header_value: &HeaderValue) -> Result<String, Error> {
 | 
				
			|||
	let s = header_value
 | 
				
			|||
		.to_str()
 | 
				
			|||
		.map_err(|_| Error::from("invalid header format"))?;
 | 
				
			|||
	Ok(s.to_string())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_client(root_certs: &[String]) -> Result<Client, Error> {
 | 
				
			|||
	let useragent = format!(
 | 
				
			|||
		"{}/{} ({}) {}",
 | 
				
			|||
		crate::APP_NAME,
 | 
				
			|||
		crate::APP_VERSION,
 | 
				
			|||
		env!("ACMED_TARGET"),
 | 
				
			|||
		env!("ACMED_HTTP_LIB_AGENT")
 | 
				
			|||
	);
 | 
				
			|||
	// TODO: allow to change the language
 | 
				
			|||
	let mut client_builder = ClientBuilder::new();
 | 
				
			|||
	let mut default_headers = HeaderMap::new();
 | 
				
			|||
	default_headers.append(header::ACCEPT_LANGUAGE, "en-US,en;q=0.5".parse().unwrap());
 | 
				
			|||
	default_headers.append(header::USER_AGENT, useragent.parse().unwrap());
 | 
				
			|||
	client_builder = client_builder.default_headers(default_headers);
 | 
				
			|||
	for crt_file in root_certs.iter() {
 | 
				
			|||
		#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
		{
 | 
				
			|||
			let mut buff = Vec::new();
 | 
				
			|||
			File::open(crt_file)
 | 
				
			|||
				.map_err(|e| Error::from(e).prefix(crt_file))?
 | 
				
			|||
				.read_to_end(&mut buff)?;
 | 
				
			|||
			let crt = reqwest::Certificate::from_pem(&buff)?;
 | 
				
			|||
			client_builder = client_builder.add_root_certificate(crt);
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
	Ok(client_builder.build()?)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get(endpoint: &mut Endpoint, url: &str) -> Result<ValidHttpResponse, HttpError> {
 | 
				
			|||
	let client = get_client(&endpoint.root_certificates)?;
 | 
				
			|||
	rate_limit(endpoint).await;
 | 
				
			|||
	let response = client
 | 
				
			|||
		.get(url)
 | 
				
			|||
		.header(header::ACCEPT, CONTENT_TYPE_JSON)
 | 
				
			|||
		.send()
 | 
				
			|||
		.await?;
 | 
				
			|||
	update_nonce(endpoint, &response)?;
 | 
				
			|||
	check_status(&response)?;
 | 
				
			|||
	ValidHttpResponse::from_response(response)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(HttpError::from)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn post<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	url: &str,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
	content_type: &str,
 | 
				
			|||
	accept: &str,
 | 
				
			|||
) -> Result<ValidHttpResponse, HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	let client = get_client(&endpoint.root_certificates)?;
 | 
				
			|||
	if endpoint.nonce.is_none() {
 | 
				
			|||
		let _ = new_nonce(endpoint).await;
 | 
				
			|||
	}
 | 
				
			|||
	for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY {
 | 
				
			|||
		let mut request = client.post(url);
 | 
				
			|||
		request = request.header(header::ACCEPT, accept);
 | 
				
			|||
		request = request.header(header::CONTENT_TYPE, content_type);
 | 
				
			|||
		let nonce = &endpoint.nonce.clone().unwrap_or_default();
 | 
				
			|||
		let body = data_builder(nonce, url)?;
 | 
				
			|||
		rate_limit(endpoint).await;
 | 
				
			|||
		log::trace!("POST request body: {body}");
 | 
				
			|||
		let response = request.body(body).send().await?;
 | 
				
			|||
		update_nonce(endpoint, &response)?;
 | 
				
			|||
		match check_status(&response) {
 | 
				
			|||
			Ok(_) => {
 | 
				
			|||
				return ValidHttpResponse::from_response(response)
 | 
				
			|||
					.await
 | 
				
			|||
					.map_err(HttpError::from);
 | 
				
			|||
			}
 | 
				
			|||
			Err(_) => {
 | 
				
			|||
				let resp = ValidHttpResponse::from_response(response).await?;
 | 
				
			|||
				let api_err = resp.json::<HttpApiError>()?;
 | 
				
			|||
				let acme_err = api_err.get_acme_type();
 | 
				
			|||
				if !acme_err.is_recoverable() {
 | 
				
			|||
					return Err(api_err.into());
 | 
				
			|||
				}
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		thread::sleep(time::Duration::from_secs(crate::DEFAULT_HTTP_FAIL_WAIT_SEC));
 | 
				
			|||
	}
 | 
				
			|||
	Err("too much errors, will not retry".into())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn post_jose<F>(
 | 
				
			|||
	endpoint: &mut Endpoint,
 | 
				
			|||
	url: &str,
 | 
				
			|||
	data_builder: &F,
 | 
				
			|||
) -> Result<ValidHttpResponse, HttpError>
 | 
				
			|||
where
 | 
				
			|||
	F: Fn(&str, &str) -> Result<String, Error>,
 | 
				
			|||
{
 | 
				
			|||
	post(
 | 
				
			|||
		endpoint,
 | 
				
			|||
		url,
 | 
				
			|||
		data_builder,
 | 
				
			|||
		CONTENT_TYPE_JOSE,
 | 
				
			|||
		CONTENT_TYPE_JSON,
 | 
				
			|||
	)
 | 
				
			|||
	.await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::is_nonce;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_nonce_valid() {
 | 
				
			|||
		let lst = [
 | 
				
			|||
			"XFHw3qcgFNZAdw",
 | 
				
			|||
			"XFHw3qcg-NZAdw",
 | 
				
			|||
			"XFHw3qcg_NZAdw",
 | 
				
			|||
			"XFHw3qcg-_ZAdw",
 | 
				
			|||
			"a",
 | 
				
			|||
			"1",
 | 
				
			|||
			"-",
 | 
				
			|||
			"_",
 | 
				
			|||
		];
 | 
				
			|||
		for n in lst.iter() {
 | 
				
			|||
			assert!(is_nonce(n));
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_nonce_invalid() {
 | 
				
			|||
		let lst = [
 | 
				
			|||
			"",
 | 
				
			|||
			"rdo9x8gS4K/mZg==",
 | 
				
			|||
			"rdo9x8gS4K/mZg",
 | 
				
			|||
			"rdo9x8gS4K+mZg",
 | 
				
			|||
			"৬",
 | 
				
			|||
			"京",
 | 
				
			|||
		];
 | 
				
			|||
		for n in lst.iter() {
 | 
				
			|||
			assert!(!is_nonce(n));
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,147 +0,0 @@ | 
				
			|||
use crate::acme_proto::Challenge;
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use acme_common::to_idna;
 | 
				
			|||
use serde::{Deserialize, Serialize};
 | 
				
			|||
use std::collections::HashMap;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::net::IpAddr;
 | 
				
			|||
use std::str::FromStr;
 | 
				
			|||
 | 
				
			|||
// RFC 3596, section 2.5
 | 
				
			|||
fn u8_to_nibbles_string(value: &u8) -> String {
 | 
				
			|||
	let bytes = value.to_ne_bytes();
 | 
				
			|||
	let first = bytes[0] & 0x0f;
 | 
				
			|||
	let second = (bytes[0] >> 4) & 0x0f;
 | 
				
			|||
	format!("{first:x}.{second:x}")
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
 | 
				
			|||
pub enum IdentifierType {
 | 
				
			|||
	#[serde(rename = "dns")]
 | 
				
			|||
	Dns,
 | 
				
			|||
	#[serde(rename = "ip")]
 | 
				
			|||
	Ip,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl IdentifierType {
 | 
				
			|||
	pub fn supported_challenges(&self) -> Vec<Challenge> {
 | 
				
			|||
		match self {
 | 
				
			|||
			IdentifierType::Dns => vec![Challenge::Http01, Challenge::Dns01, Challenge::TlsAlpn01],
 | 
				
			|||
			IdentifierType::Ip => vec![Challenge::Http01, Challenge::TlsAlpn01],
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for IdentifierType {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let name = match self {
 | 
				
			|||
			IdentifierType::Dns => "dns",
 | 
				
			|||
			IdentifierType::Ip => "ip",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{name}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct Identifier {
 | 
				
			|||
	pub id_type: IdentifierType,
 | 
				
			|||
	pub value: String,
 | 
				
			|||
	pub challenge: Challenge,
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl Identifier {
 | 
				
			|||
	pub fn new(
 | 
				
			|||
		id_type: IdentifierType,
 | 
				
			|||
		value: &str,
 | 
				
			|||
		challenge: &str,
 | 
				
			|||
		env: &HashMap<String, String>,
 | 
				
			|||
	) -> Result<Self, Error> {
 | 
				
			|||
		let value = match id_type {
 | 
				
			|||
			IdentifierType::Dns => to_idna(value)?,
 | 
				
			|||
			IdentifierType::Ip => IpAddr::from_str(value)?.to_string(),
 | 
				
			|||
		};
 | 
				
			|||
		let challenge = Challenge::from_str(challenge)?;
 | 
				
			|||
		if !id_type.supported_challenges().contains(&challenge) {
 | 
				
			|||
			let msg =
 | 
				
			|||
				format!("challenge {challenge} cannot be used with identifier of type {id_type}");
 | 
				
			|||
			return Err(msg.into());
 | 
				
			|||
		}
 | 
				
			|||
		Ok(Identifier {
 | 
				
			|||
			id_type,
 | 
				
			|||
			value,
 | 
				
			|||
			challenge,
 | 
				
			|||
			env: env.clone(),
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub fn get_tls_alpn_name(&self) -> Result<String, Error> {
 | 
				
			|||
		match &self.id_type {
 | 
				
			|||
			IdentifierType::Dns => Ok(self.value.to_owned()),
 | 
				
			|||
			IdentifierType::Ip => match IpAddr::from_str(&self.value)? {
 | 
				
			|||
				IpAddr::V4(ip) => {
 | 
				
			|||
					let dn = ip
 | 
				
			|||
						.octets()
 | 
				
			|||
						.iter()
 | 
				
			|||
						.rev()
 | 
				
			|||
						.map(|v| v.to_string())
 | 
				
			|||
						.collect::<Vec<String>>()
 | 
				
			|||
						.join(".");
 | 
				
			|||
					let dn = format!("{dn}.in-addr.arpa");
 | 
				
			|||
					Ok(dn)
 | 
				
			|||
				}
 | 
				
			|||
				IpAddr::V6(ip) => {
 | 
				
			|||
					let dn = ip
 | 
				
			|||
						.octets()
 | 
				
			|||
						.iter()
 | 
				
			|||
						.rev()
 | 
				
			|||
						.map(u8_to_nibbles_string)
 | 
				
			|||
						.collect::<Vec<String>>()
 | 
				
			|||
						.join(".");
 | 
				
			|||
					let dn = format!("{dn}.ip6.arpa");
 | 
				
			|||
					Ok(dn)
 | 
				
			|||
				}
 | 
				
			|||
			},
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for Identifier {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		write!(f, "{}: {} ({})", self.id_type, self.value, self.challenge)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::*;
 | 
				
			|||
	use std::collections::HashMap;
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_ipv4_tls_alpn_name() {
 | 
				
			|||
		let env = HashMap::new();
 | 
				
			|||
		let id = Identifier::new(IdentifierType::Ip, "203.0.113.1", "http-01", &env).unwrap();
 | 
				
			|||
		assert_eq!(&id.get_tls_alpn_name().unwrap(), "1.113.0.203.in-addr.arpa");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_ipv6_tls_alpn_name() {
 | 
				
			|||
		let env = HashMap::new();
 | 
				
			|||
		let id = Identifier::new(IdentifierType::Ip, "2001:db8::1", "http-01", &env).unwrap();
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			&id.get_tls_alpn_name().unwrap(),
 | 
				
			|||
			"1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"
 | 
				
			|||
		);
 | 
				
			|||
		let id = Identifier::new(
 | 
				
			|||
			IdentifierType::Ip,
 | 
				
			|||
			"4321:0:1:2:3:4:567:89ab",
 | 
				
			|||
			"http-01",
 | 
				
			|||
			&env,
 | 
				
			|||
		)
 | 
				
			|||
		.unwrap();
 | 
				
			|||
		assert_eq!(
 | 
				
			|||
			&id.get_tls_alpn_name().unwrap(),
 | 
				
			|||
			"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa"
 | 
				
			|||
		);
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,191 +0,0 @@ | 
				
			|||
use acme_common::b64_encode;
 | 
				
			|||
use acme_common::crypto::{HashFunction, JwsSignatureAlgorithm, KeyPair};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::Serialize;
 | 
				
			|||
use serde_json::value::Value;
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
struct JwsData {
 | 
				
			|||
	protected: String,
 | 
				
			|||
	payload: String,
 | 
				
			|||
	signature: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Serialize)]
 | 
				
			|||
struct JwsProtectedHeader {
 | 
				
			|||
	alg: String,
 | 
				
			|||
	#[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			|||
	jwk: Option<Value>,
 | 
				
			|||
	#[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			|||
	kid: Option<String>,
 | 
				
			|||
	#[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			|||
	nonce: Option<String>,
 | 
				
			|||
	url: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_jws_data(
 | 
				
			|||
	key_pair: &KeyPair,
 | 
				
			|||
	sign_alg: &JwsSignatureAlgorithm,
 | 
				
			|||
	protected: &str,
 | 
				
			|||
	payload: &[u8],
 | 
				
			|||
) -> Result<String, Error> {
 | 
				
			|||
	let protected = b64_encode(protected);
 | 
				
			|||
	let payload = b64_encode(payload);
 | 
				
			|||
	let signing_input = format!("{protected}.{payload}");
 | 
				
			|||
	let signature = key_pair.sign(sign_alg, signing_input.as_bytes())?;
 | 
				
			|||
	let signature = b64_encode(&signature);
 | 
				
			|||
	let data = JwsData {
 | 
				
			|||
		protected,
 | 
				
			|||
		payload,
 | 
				
			|||
		signature,
 | 
				
			|||
	};
 | 
				
			|||
	let str_data = serde_json::to_string(&data)?;
 | 
				
			|||
	Ok(str_data)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn encode_jwk(
 | 
				
			|||
	key_pair: &KeyPair,
 | 
				
			|||
	sign_alg: &JwsSignatureAlgorithm,
 | 
				
			|||
	payload: &[u8],
 | 
				
			|||
	url: &str,
 | 
				
			|||
	nonce: Option<String>,
 | 
				
			|||
) -> Result<String, Error> {
 | 
				
			|||
	let protected = JwsProtectedHeader {
 | 
				
			|||
		alg: sign_alg.to_string(),
 | 
				
			|||
		jwk: Some(key_pair.jwk_public_key()?),
 | 
				
			|||
		kid: None,
 | 
				
			|||
		nonce,
 | 
				
			|||
		url: url.into(),
 | 
				
			|||
	};
 | 
				
			|||
	let protected = serde_json::to_string(&protected)?;
 | 
				
			|||
	get_jws_data(key_pair, sign_alg, &protected, payload)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn encode_kid(
 | 
				
			|||
	key_pair: &KeyPair,
 | 
				
			|||
	sign_alg: &JwsSignatureAlgorithm,
 | 
				
			|||
	key_id: &str,
 | 
				
			|||
	payload: &[u8],
 | 
				
			|||
	url: &str,
 | 
				
			|||
	nonce: &str,
 | 
				
			|||
) -> Result<String, Error> {
 | 
				
			|||
	let protected = JwsProtectedHeader {
 | 
				
			|||
		alg: sign_alg.to_string(),
 | 
				
			|||
		jwk: None,
 | 
				
			|||
		kid: Some(key_id.to_string()),
 | 
				
			|||
		nonce: Some(nonce.into()),
 | 
				
			|||
		url: url.into(),
 | 
				
			|||
	};
 | 
				
			|||
	let protected = serde_json::to_string(&protected)?;
 | 
				
			|||
	get_jws_data(key_pair, sign_alg, &protected, payload)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn encode_kid_mac(
 | 
				
			|||
	key: &[u8],
 | 
				
			|||
	sign_alg: &JwsSignatureAlgorithm,
 | 
				
			|||
	key_id: &str,
 | 
				
			|||
	payload: &[u8],
 | 
				
			|||
	url: &str,
 | 
				
			|||
) -> Result<String, Error> {
 | 
				
			|||
	let protected = JwsProtectedHeader {
 | 
				
			|||
		alg: sign_alg.to_string(),
 | 
				
			|||
		jwk: None,
 | 
				
			|||
		kid: Some(key_id.to_string()),
 | 
				
			|||
		nonce: None,
 | 
				
			|||
		url: url.into(),
 | 
				
			|||
	};
 | 
				
			|||
	let protected = serde_json::to_string(&protected)?;
 | 
				
			|||
	let protected = b64_encode(&protected);
 | 
				
			|||
	let payload = b64_encode(payload);
 | 
				
			|||
	let signing_input = format!("{protected}.{payload}");
 | 
				
			|||
	let hash_func = match sign_alg {
 | 
				
			|||
		JwsSignatureAlgorithm::Hs256 => HashFunction::Sha256,
 | 
				
			|||
		JwsSignatureAlgorithm::Hs384 => HashFunction::Sha384,
 | 
				
			|||
		JwsSignatureAlgorithm::Hs512 => HashFunction::Sha512,
 | 
				
			|||
		_ => {
 | 
				
			|||
			return Err(format!("{sign_alg}: not a HMAC-based signature algorithm").into());
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	let signature = hash_func.hmac(key, signing_input.as_bytes())?;
 | 
				
			|||
	let signature = b64_encode(&signature);
 | 
				
			|||
	let data = JwsData {
 | 
				
			|||
		protected,
 | 
				
			|||
		payload,
 | 
				
			|||
		signature,
 | 
				
			|||
	};
 | 
				
			|||
	let str_data = serde_json::to_string(&data)?;
 | 
				
			|||
	Ok(str_data)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::{encode_jwk, encode_kid};
 | 
				
			|||
	use acme_common::crypto::{gen_keypair, KeyType};
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_default_jwk() {
 | 
				
			|||
		let key_type = KeyType::EcdsaP256;
 | 
				
			|||
		let key_pair = gen_keypair(key_type).unwrap();
 | 
				
			|||
		let payload = "Dummy payload 1";
 | 
				
			|||
		let payload_b64 = "RHVtbXkgcGF5bG9hZCAx";
 | 
				
			|||
		let s = encode_jwk(
 | 
				
			|||
			&key_pair,
 | 
				
			|||
			&key_type.get_default_signature_alg(),
 | 
				
			|||
			payload.as_bytes(),
 | 
				
			|||
			"",
 | 
				
			|||
			Some(String::new()),
 | 
				
			|||
		);
 | 
				
			|||
		assert!(s.is_ok());
 | 
				
			|||
		let s = s.unwrap();
 | 
				
			|||
		assert!(s.contains("\"protected\""));
 | 
				
			|||
		assert!(s.contains("\"payload\""));
 | 
				
			|||
		assert!(s.contains("\"signature\""));
 | 
				
			|||
		assert!(s.contains(payload_b64));
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_default_nopad_jwk() {
 | 
				
			|||
		let key_type = KeyType::EcdsaP256;
 | 
				
			|||
		let key_pair = gen_keypair(key_type).unwrap();
 | 
				
			|||
		let payload = "Dummy payload";
 | 
				
			|||
		let payload_b64 = "RHVtbXkgcGF5bG9hZA";
 | 
				
			|||
		let payload_b64_pad = "RHVtbXkgcGF5bG9hZA==";
 | 
				
			|||
		let s = encode_jwk(
 | 
				
			|||
			&key_pair,
 | 
				
			|||
			&key_type.get_default_signature_alg(),
 | 
				
			|||
			payload.as_bytes(),
 | 
				
			|||
			"",
 | 
				
			|||
			Some(String::new()),
 | 
				
			|||
		);
 | 
				
			|||
		assert!(s.is_ok());
 | 
				
			|||
		let s = s.unwrap();
 | 
				
			|||
		assert!(s.contains("\"protected\""));
 | 
				
			|||
		assert!(s.contains("\"payload\""));
 | 
				
			|||
		assert!(s.contains("\"signature\""));
 | 
				
			|||
		assert!(s.contains(payload_b64));
 | 
				
			|||
		assert!(!s.contains(payload_b64_pad));
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_default_kid() {
 | 
				
			|||
		let key_type = KeyType::EcdsaP256;
 | 
				
			|||
		let key_pair = gen_keypair(key_type).unwrap();
 | 
				
			|||
		let payload = "Dummy payload 1";
 | 
				
			|||
		let payload_b64 = "RHVtbXkgcGF5bG9hZCAx";
 | 
				
			|||
		let key_id = "0x2a";
 | 
				
			|||
		let s = encode_kid(
 | 
				
			|||
			&key_pair,
 | 
				
			|||
			&key_type.get_default_signature_alg(),
 | 
				
			|||
			key_id,
 | 
				
			|||
			payload.as_bytes(),
 | 
				
			|||
			"",
 | 
				
			|||
			"",
 | 
				
			|||
		);
 | 
				
			|||
		assert!(s.is_ok());
 | 
				
			|||
		let s = s.unwrap();
 | 
				
			|||
		assert!(s.contains("\"protected\""));
 | 
				
			|||
		assert!(s.contains("\"payload\""));
 | 
				
			|||
		assert!(s.contains("\"signature\""));
 | 
				
			|||
		assert!(s.contains(payload_b64));
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -1,6 +0,0 @@ | 
				
			|||
pub trait HasLogger {
 | 
				
			|||
	fn warn(&self, msg: &str);
 | 
				
			|||
	fn info(&self, msg: &str);
 | 
				
			|||
	fn debug(&self, msg: &str);
 | 
				
			|||
	fn trace(&self, msg: &str);
 | 
				
			|||
}
 | 
				
			|||
@ -1,180 +0,0 @@ | 
				
			|||
use crate::main_event_loop::MainEventLoop;
 | 
				
			|||
use acme_common::crypto::{
 | 
				
			|||
	get_lib_name, get_lib_version, HashFunction, JwsSignatureAlgorithm, KeyType,
 | 
				
			|||
};
 | 
				
			|||
use acme_common::logs::{set_log_system, DEFAULT_LOG_LEVEL};
 | 
				
			|||
use acme_common::{clean_pid_file, init_server};
 | 
				
			|||
use async_lock::RwLock;
 | 
				
			|||
use clap::{Arg, ArgAction, Command};
 | 
				
			|||
use log::error;
 | 
				
			|||
use std::sync::Arc;
 | 
				
			|||
use tokio::runtime::Builder;
 | 
				
			|||
 | 
				
			|||
mod account;
 | 
				
			|||
mod acme_proto;
 | 
				
			|||
mod certificate;
 | 
				
			|||
mod config;
 | 
				
			|||
mod duration;
 | 
				
			|||
mod endpoint;
 | 
				
			|||
mod hooks;
 | 
				
			|||
mod http;
 | 
				
			|||
mod identifier;
 | 
				
			|||
mod jws;
 | 
				
			|||
mod logs;
 | 
				
			|||
mod main_event_loop;
 | 
				
			|||
mod storage;
 | 
				
			|||
mod template;
 | 
				
			|||
 | 
				
			|||
pub const APP_NAME: &str = "ACMEd";
 | 
				
			|||
pub const APP_THREAD_NAME: &str = "acmed-runtime";
 | 
				
			|||
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
 | 
				
			|||
pub const DEFAULT_ACCOUNTS_DIR: &str = env!("ACMED_DEFAULT_ACCOUNTS_DIR");
 | 
				
			|||
pub const DEFAULT_CERT_DIR: &str = env!("ACMED_DEFAULT_CERT_DIR");
 | 
				
			|||
pub const DEFAULT_CERT_FORMAT: &str = env!("ACMED_DEFAULT_CERT_FORMAT");
 | 
				
			|||
pub const DEFAULT_CONFIG_FILE: &str = env!("ACMED_DEFAULT_CONFIG_FILE");
 | 
				
			|||
pub const DEFAULT_PID_FILE: &str = env!("ACMED_DEFAULT_PID_FILE");
 | 
				
			|||
pub const DEFAULT_POOL_TIME: u64 = 5000;
 | 
				
			|||
pub const DEFAULT_CSR_DIGEST: HashFunction = HashFunction::Sha256;
 | 
				
			|||
pub const DEFAULT_CERT_KEY_TYPE: KeyType = KeyType::Rsa2048;
 | 
				
			|||
pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644;
 | 
				
			|||
pub const DEFAULT_CERT_RANDOM_EARLY_RENEW: u64 = 0; // default to not renewing early
 | 
				
			|||
pub const DEFAULT_CERT_RENEW_DELAY: u64 = 30 * 24 * 60 * 60; // 30 days
 | 
				
			|||
pub const DEFAULT_PK_FILE_MODE: u32 = 0o600;
 | 
				
			|||
pub const DEFAULT_ACCOUNT_FILE_MODE: u32 = 0o600;
 | 
				
			|||
pub const DEFAULT_KP_REUSE: bool = false;
 | 
				
			|||
pub const DEFAULT_ACCOUNT_KEY_TYPE: KeyType = KeyType::EcdsaP256;
 | 
				
			|||
pub const DEFAULT_EXTERNAL_ACCOUNT_JWA: JwsSignatureAlgorithm = JwsSignatureAlgorithm::Hs256;
 | 
				
			|||
pub const DEFAULT_POOL_NB_TRIES: usize = 20;
 | 
				
			|||
pub const DEFAULT_POOL_WAIT_SEC: u64 = 5;
 | 
				
			|||
pub const DEFAULT_HTTP_FAIL_NB_RETRY: usize = 10;
 | 
				
			|||
pub const DEFAULT_HTTP_FAIL_WAIT_SEC: u64 = 1;
 | 
				
			|||
pub const DEFAULT_HOOK_ALLOW_FAILURE: bool = false;
 | 
				
			|||
pub const MAX_RATE_LIMIT_SLEEP_MILISEC: u64 = 3_600_000;
 | 
				
			|||
pub const MIN_RATE_LIMIT_SLEEP_MILISEC: u64 = 100;
 | 
				
			|||
 | 
				
			|||
type AccountSync = Arc<RwLock<account::Account>>;
 | 
				
			|||
type EndpointSync = Arc<RwLock<endpoint::Endpoint>>;
 | 
				
			|||
 | 
				
			|||
fn main() {
 | 
				
			|||
	Builder::new_multi_thread()
 | 
				
			|||
		.enable_all()
 | 
				
			|||
		.thread_name(APP_THREAD_NAME)
 | 
				
			|||
		.build()
 | 
				
			|||
		.unwrap()
 | 
				
			|||
		.block_on(inner_main());
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn inner_main() {
 | 
				
			|||
	let full_version = format!(
 | 
				
			|||
		"{APP_VERSION} built for {}\n\nCryptographic library:\n - {} {}\nHTTP client library:\n - {} {}",
 | 
				
			|||
		env!("ACMED_TARGET"),
 | 
				
			|||
		get_lib_name(),
 | 
				
			|||
		get_lib_version(),
 | 
				
			|||
		env!("ACMED_HTTP_LIB_NAME"),
 | 
				
			|||
		env!("ACMED_HTTP_LIB_VERSION")
 | 
				
			|||
	);
 | 
				
			|||
	let default_log_level = DEFAULT_LOG_LEVEL.to_string().to_lowercase();
 | 
				
			|||
	let matches = Command::new(APP_NAME)
 | 
				
			|||
		.version(APP_VERSION)
 | 
				
			|||
		.long_version(full_version)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("config")
 | 
				
			|||
				.short('c')
 | 
				
			|||
				.long("config")
 | 
				
			|||
				.help("Path to the main configuration file")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("FILE")
 | 
				
			|||
				.default_value(DEFAULT_CONFIG_FILE),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("log-level")
 | 
				
			|||
				.long("log-level")
 | 
				
			|||
				.help("Specify the log level")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("LEVEL")
 | 
				
			|||
				.value_parser(["error", "warn", "info", "debug", "trace"])
 | 
				
			|||
				.default_value(default_log_level),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("to-syslog")
 | 
				
			|||
				.long("log-syslog")
 | 
				
			|||
				.help("Sends log messages via syslog")
 | 
				
			|||
				.conflicts_with("to-stderr")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("to-stderr")
 | 
				
			|||
				.long("log-stderr")
 | 
				
			|||
				.help("Prints log messages to the standard error output")
 | 
				
			|||
				.conflicts_with("to-syslog")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("foreground")
 | 
				
			|||
				.short('f')
 | 
				
			|||
				.long("foreground")
 | 
				
			|||
				.help("Runs in the foreground")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("pid-file")
 | 
				
			|||
				.long("pid-file")
 | 
				
			|||
				.help("Path to the PID file")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("FILE")
 | 
				
			|||
				.default_value(DEFAULT_PID_FILE)
 | 
				
			|||
				.default_value_if("no-pid-file", clap::builder::ArgPredicate::IsPresent, None)
 | 
				
			|||
				.conflicts_with("no-pid-file"),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("no-pid-file")
 | 
				
			|||
				.long("no-pid-file")
 | 
				
			|||
				.help("Do not create any PID file")
 | 
				
			|||
				.conflicts_with("pid-file")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("root-cert")
 | 
				
			|||
				.long("root-cert")
 | 
				
			|||
				.help("Add a root certificate to the trust store (can be set multiple times)")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.action(ArgAction::Append)
 | 
				
			|||
				.value_name("FILE"),
 | 
				
			|||
		)
 | 
				
			|||
		.get_matches();
 | 
				
			|||
 | 
				
			|||
	match set_log_system(
 | 
				
			|||
		matches.get_one::<String>("log-level").map(|e| e.as_str()),
 | 
				
			|||
		matches.get_flag("to-syslog"),
 | 
				
			|||
		matches.get_flag("to-stderr"),
 | 
				
			|||
	) {
 | 
				
			|||
		Ok(_) => {}
 | 
				
			|||
		Err(e) => {
 | 
				
			|||
			eprintln!("Error: {e}");
 | 
				
			|||
			std::process::exit(2);
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
 | 
				
			|||
	let root_certs = match matches.get_many::<String>("root-cert") {
 | 
				
			|||
		Some(v) => v.map(|e| e.as_str()).collect(),
 | 
				
			|||
		None => vec![],
 | 
				
			|||
	};
 | 
				
			|||
 | 
				
			|||
	let config_file = matches
 | 
				
			|||
		.get_one::<String>("config")
 | 
				
			|||
		.map(|e| e.as_str())
 | 
				
			|||
		.unwrap_or(DEFAULT_CONFIG_FILE);
 | 
				
			|||
	let pid_file = matches.get_one::<String>("pid-file").map(|e| e.as_str());
 | 
				
			|||
 | 
				
			|||
	init_server(matches.get_flag("foreground"), pid_file);
 | 
				
			|||
 | 
				
			|||
	let mut srv = match MainEventLoop::new(config_file, &root_certs).await {
 | 
				
			|||
		Ok(s) => s,
 | 
				
			|||
		Err(e) => {
 | 
				
			|||
			error!("{e}");
 | 
				
			|||
			let _ = clean_pid_file(pid_file);
 | 
				
			|||
			std::process::exit(1);
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	srv.run().await;
 | 
				
			|||
}
 | 
				
			|||
@ -1,223 +0,0 @@ | 
				
			|||
use crate::account::Account;
 | 
				
			|||
use crate::acme_proto::request_certificate;
 | 
				
			|||
use crate::certificate::Certificate;
 | 
				
			|||
use crate::config;
 | 
				
			|||
use crate::endpoint::Endpoint;
 | 
				
			|||
use crate::hooks::HookType;
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::storage::FileManager;
 | 
				
			|||
use crate::{AccountSync, EndpointSync};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use async_lock::RwLock;
 | 
				
			|||
use futures::stream::FuturesUnordered;
 | 
				
			|||
use futures::StreamExt;
 | 
				
			|||
use std::collections::HashMap;
 | 
				
			|||
use std::sync::Arc;
 | 
				
			|||
use std::time::Duration;
 | 
				
			|||
use tokio::time::sleep;
 | 
				
			|||
 | 
				
			|||
pub struct MainEventLoop {
 | 
				
			|||
	certificates: HashMap<String, Certificate>,
 | 
				
			|||
	accounts: HashMap<String, AccountSync>,
 | 
				
			|||
	endpoints: HashMap<String, EndpointSync>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl MainEventLoop {
 | 
				
			|||
	pub async fn new(config_file: &str, root_certs: &[&str]) -> Result<Self, Error> {
 | 
				
			|||
		let cnf = config::from_file(config_file)?;
 | 
				
			|||
		let file_hooks = vec![
 | 
				
			|||
			HookType::FilePreCreate,
 | 
				
			|||
			HookType::FilePostCreate,
 | 
				
			|||
			HookType::FilePreEdit,
 | 
				
			|||
			HookType::FilePostEdit,
 | 
				
			|||
		]
 | 
				
			|||
		.into_iter()
 | 
				
			|||
		.collect();
 | 
				
			|||
		let cert_hooks = vec![
 | 
				
			|||
			HookType::ChallengeHttp01,
 | 
				
			|||
			HookType::ChallengeHttp01Clean,
 | 
				
			|||
			HookType::ChallengeDns01,
 | 
				
			|||
			HookType::ChallengeDns01Clean,
 | 
				
			|||
			HookType::ChallengeTlsAlpn01,
 | 
				
			|||
			HookType::ChallengeTlsAlpn01Clean,
 | 
				
			|||
			HookType::PostOperation,
 | 
				
			|||
		]
 | 
				
			|||
		.into_iter()
 | 
				
			|||
		.collect();
 | 
				
			|||
 | 
				
			|||
		let mut accounts: HashMap<String, Account> = HashMap::new();
 | 
				
			|||
		for acc in &cnf.account {
 | 
				
			|||
			let fm = FileManager {
 | 
				
			|||
				account_directory: cnf.get_account_dir(),
 | 
				
			|||
				account_name: acc.name.clone(),
 | 
				
			|||
				crt_name: String::new(),
 | 
				
			|||
				crt_name_format: String::new(),
 | 
				
			|||
				crt_directory: String::new(),
 | 
				
			|||
				crt_key_type: String::new(),
 | 
				
			|||
				cert_file_mode: cnf.get_cert_file_mode(),
 | 
				
			|||
				cert_file_owner: cnf.get_cert_file_user(),
 | 
				
			|||
				cert_file_group: cnf.get_cert_file_group(),
 | 
				
			|||
				cert_file_ext: cnf.get_cert_file_ext(),
 | 
				
			|||
				pk_file_mode: cnf.get_pk_file_mode(),
 | 
				
			|||
				pk_file_owner: cnf.get_pk_file_user(),
 | 
				
			|||
				pk_file_group: cnf.get_pk_file_group(),
 | 
				
			|||
				pk_file_ext: cnf.get_pk_file_ext(),
 | 
				
			|||
				hooks: acc
 | 
				
			|||
					.get_hooks(&cnf)?
 | 
				
			|||
					.iter()
 | 
				
			|||
					.filter(|h| !h.hook_type.is_disjoint(&file_hooks))
 | 
				
			|||
					.map(|e| e.to_owned())
 | 
				
			|||
					.collect(),
 | 
				
			|||
				env: acc.env.clone(),
 | 
				
			|||
			};
 | 
				
			|||
			let account = acc.to_generic(&fm).await?;
 | 
				
			|||
			let name = acc.name.clone();
 | 
				
			|||
			accounts.insert(name, account);
 | 
				
			|||
		}
 | 
				
			|||
 | 
				
			|||
		let mut endpoints: HashMap<String, Endpoint> = HashMap::new();
 | 
				
			|||
		let mut certificates: HashMap<String, Certificate> = HashMap::new();
 | 
				
			|||
		for crt in cnf.certificate.iter() {
 | 
				
			|||
			let endpoint = crt.get_endpoint(&cnf, root_certs)?;
 | 
				
			|||
			let endpoint_name = endpoint.name.clone();
 | 
				
			|||
			let crt_name = crt.get_crt_name()?;
 | 
				
			|||
			let key_type = crt.get_key_type()?;
 | 
				
			|||
			let hooks = crt.get_hooks(&cnf)?;
 | 
				
			|||
			let fm = FileManager {
 | 
				
			|||
				account_directory: cnf.get_account_dir(),
 | 
				
			|||
				account_name: crt.account.clone(),
 | 
				
			|||
				crt_name: crt_name.clone(),
 | 
				
			|||
				crt_name_format: crt.get_crt_name_format(&cnf)?,
 | 
				
			|||
				crt_directory: crt.get_crt_dir(&cnf),
 | 
				
			|||
				crt_key_type: key_type.to_string(),
 | 
				
			|||
				cert_file_mode: cnf.get_cert_file_mode(),
 | 
				
			|||
				cert_file_owner: cnf.get_cert_file_user(),
 | 
				
			|||
				cert_file_group: cnf.get_cert_file_group(),
 | 
				
			|||
				cert_file_ext: cnf.get_cert_file_ext(),
 | 
				
			|||
				pk_file_mode: cnf.get_pk_file_mode(),
 | 
				
			|||
				pk_file_owner: cnf.get_pk_file_user(),
 | 
				
			|||
				pk_file_group: cnf.get_pk_file_group(),
 | 
				
			|||
				pk_file_ext: cnf.get_pk_file_ext(),
 | 
				
			|||
				hooks: hooks
 | 
				
			|||
					.iter()
 | 
				
			|||
					.filter(|h| !h.hook_type.is_disjoint(&file_hooks))
 | 
				
			|||
					.map(|e| e.to_owned())
 | 
				
			|||
					.collect(),
 | 
				
			|||
				env: crt.env.clone(),
 | 
				
			|||
			};
 | 
				
			|||
			let cert = Certificate {
 | 
				
			|||
				account_name: crt.account.clone(),
 | 
				
			|||
				identifiers: crt.get_identifiers()?,
 | 
				
			|||
				subject_attributes: crt.subject_attributes.to_generic(),
 | 
				
			|||
				key_type,
 | 
				
			|||
				csr_digest: crt.get_csr_digest()?,
 | 
				
			|||
				kp_reuse: crt.get_kp_reuse(),
 | 
				
			|||
				endpoint_name: endpoint_name.clone(),
 | 
				
			|||
				hooks: hooks
 | 
				
			|||
					.iter()
 | 
				
			|||
					.filter(|h| !h.hook_type.is_disjoint(&cert_hooks))
 | 
				
			|||
					.map(|e| e.to_owned())
 | 
				
			|||
					.collect(),
 | 
				
			|||
				crt_name,
 | 
				
			|||
				env: crt.env.to_owned(),
 | 
				
			|||
				random_early_renew: crt.get_random_early_renew(&cnf)?,
 | 
				
			|||
				renew_delay: crt.get_renew_delay(&cnf)?,
 | 
				
			|||
				file_manager: fm,
 | 
				
			|||
			};
 | 
				
			|||
			let crt_id = cert.get_id();
 | 
				
			|||
			if certificates.contains_key(&crt_id) {
 | 
				
			|||
				let msg = format!("{crt_id}: duplicate certificate id");
 | 
				
			|||
				return Err(msg.into());
 | 
				
			|||
			}
 | 
				
			|||
			match accounts.get_mut(&crt.account) {
 | 
				
			|||
				Some(acc) => acc.add_endpoint_name(&endpoint_name),
 | 
				
			|||
				None => {
 | 
				
			|||
					let msg = format!("{}: account not found", &crt.account);
 | 
				
			|||
					return Err(msg.into());
 | 
				
			|||
				}
 | 
				
			|||
			};
 | 
				
			|||
			if !endpoints.contains_key(&endpoint.name) {
 | 
				
			|||
				endpoints.insert(endpoint.name.clone(), endpoint);
 | 
				
			|||
			}
 | 
				
			|||
			certificates.insert(crt_id, cert);
 | 
				
			|||
		}
 | 
				
			|||
 | 
				
			|||
		Ok(MainEventLoop {
 | 
				
			|||
			certificates,
 | 
				
			|||
			accounts: accounts
 | 
				
			|||
				.iter()
 | 
				
			|||
				.map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned()))))
 | 
				
			|||
				.collect(),
 | 
				
			|||
			endpoints: endpoints
 | 
				
			|||
				.iter()
 | 
				
			|||
				.map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned()))))
 | 
				
			|||
				.collect(),
 | 
				
			|||
		})
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	pub async fn run(&mut self) {
 | 
				
			|||
		let mut renewals = FuturesUnordered::new();
 | 
				
			|||
		for (_, crt) in self.certificates.iter_mut() {
 | 
				
			|||
			log::trace!("Adding certificate: {}", crt.get_id());
 | 
				
			|||
			if let Some(acc) = self.accounts.get(&crt.account_name) {
 | 
				
			|||
				if let Some(ept) = self.endpoints.get(&crt.endpoint_name) {
 | 
				
			|||
					renewals.push(renew_certificate(crt, acc.clone(), ept.clone()));
 | 
				
			|||
				}
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		loop {
 | 
				
			|||
			if renewals.is_empty() {
 | 
				
			|||
				log::error!("No certificate found.");
 | 
				
			|||
				return;
 | 
				
			|||
			}
 | 
				
			|||
			if let Some((crt, acc, ept)) = renewals.next().await {
 | 
				
			|||
				renewals.push(renew_certificate(crt, acc, ept));
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn renew_certificate(
 | 
				
			|||
	certificate: &mut Certificate,
 | 
				
			|||
	account_s: AccountSync,
 | 
				
			|||
	endpoint_s: EndpointSync,
 | 
				
			|||
) -> (&mut Certificate, AccountSync, EndpointSync) {
 | 
				
			|||
	let backoff = [60, 10 * 60, 100 * 60, 24 * 60 * 60];
 | 
				
			|||
	let mut scheduling_retries = 0;
 | 
				
			|||
	loop {
 | 
				
			|||
		match certificate.schedule_renewal().await {
 | 
				
			|||
			Ok(duration) => {
 | 
				
			|||
				sleep(duration).await;
 | 
				
			|||
				break;
 | 
				
			|||
			}
 | 
				
			|||
			Err(e) => {
 | 
				
			|||
				certificate.warn(&e.message);
 | 
				
			|||
				sleep(Duration::from_secs(
 | 
				
			|||
					backoff[scheduling_retries.min(backoff.len() - 1)],
 | 
				
			|||
				))
 | 
				
			|||
				.await;
 | 
				
			|||
				scheduling_retries += 1;
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
	let (status, is_success) =
 | 
				
			|||
		match request_certificate(certificate, account_s.clone(), endpoint_s.clone()).await {
 | 
				
			|||
			Ok(_) => ("success".to_string(), true),
 | 
				
			|||
			Err(e) => {
 | 
				
			|||
				let e = e.prefix("unable to renew the certificate");
 | 
				
			|||
				certificate.warn(&e.message);
 | 
				
			|||
				(e.message, false)
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
	match certificate
 | 
				
			|||
		.call_post_operation_hooks(&status, is_success)
 | 
				
			|||
		.await
 | 
				
			|||
	{
 | 
				
			|||
		Ok(_) => {}
 | 
				
			|||
		Err(e) => {
 | 
				
			|||
			let e = e.prefix("post-operation hook error");
 | 
				
			|||
			certificate.warn(&e.message);
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	(certificate, account_s.clone(), endpoint_s.clone())
 | 
				
			|||
}
 | 
				
			|||
@ -1,312 +0,0 @@ | 
				
			|||
use crate::hooks::{self, FileStorageHookData, Hook, HookEnvData, HookType};
 | 
				
			|||
use crate::logs::HasLogger;
 | 
				
			|||
use crate::template::render_template;
 | 
				
			|||
use acme_common::b64_encode;
 | 
				
			|||
use acme_common::crypto::{KeyPair, X509Certificate};
 | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use serde::Serialize;
 | 
				
			|||
use std::collections::HashMap;
 | 
				
			|||
use std::fmt;
 | 
				
			|||
use std::path::{Path, PathBuf};
 | 
				
			|||
use tokio::fs::{File, OpenOptions};
 | 
				
			|||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Debug)]
 | 
				
			|||
pub struct FileManager {
 | 
				
			|||
	pub account_name: String,
 | 
				
			|||
	pub account_directory: String,
 | 
				
			|||
	pub crt_name: String,
 | 
				
			|||
	pub crt_name_format: String,
 | 
				
			|||
	pub crt_directory: String,
 | 
				
			|||
	pub crt_key_type: String,
 | 
				
			|||
	pub cert_file_mode: u32,
 | 
				
			|||
	pub cert_file_owner: Option<String>,
 | 
				
			|||
	pub cert_file_group: Option<String>,
 | 
				
			|||
	pub cert_file_ext: Option<String>,
 | 
				
			|||
	pub pk_file_mode: u32,
 | 
				
			|||
	pub pk_file_owner: Option<String>,
 | 
				
			|||
	pub pk_file_group: Option<String>,
 | 
				
			|||
	pub pk_file_ext: Option<String>,
 | 
				
			|||
	pub hooks: Vec<Hook>,
 | 
				
			|||
	pub env: HashMap<String, String>,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl HasLogger for FileManager {
 | 
				
			|||
	fn warn(&self, msg: &str) {
 | 
				
			|||
		log::warn!("{self}: {msg}");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn info(&self, msg: &str) {
 | 
				
			|||
		log::info!("{self}: {msg}");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn debug(&self, msg: &str) {
 | 
				
			|||
		log::debug!("{self}: {msg}");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fn trace(&self, msg: &str) {
 | 
				
			|||
		log::trace!("{self}: {msg}");
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for FileManager {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = if !self.crt_name.is_empty() {
 | 
				
			|||
			format!("certificate \"{}_{}\"", self.crt_name, self.crt_key_type)
 | 
				
			|||
		} else {
 | 
				
			|||
			format!("account \"{}\"", self.account_name)
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone)]
 | 
				
			|||
enum FileType {
 | 
				
			|||
	Account,
 | 
				
			|||
	PrivateKey,
 | 
				
			|||
	Certificate,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
impl fmt::Display for FileType {
 | 
				
			|||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			|||
		let s = match self {
 | 
				
			|||
			FileType::Account => "account",
 | 
				
			|||
			FileType::PrivateKey => "pk",
 | 
				
			|||
			FileType::Certificate => "crt",
 | 
				
			|||
		};
 | 
				
			|||
		write!(f, "{s}")
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[derive(Clone, Serialize)]
 | 
				
			|||
pub struct CertFileFormat {
 | 
				
			|||
	pub ext: String,
 | 
				
			|||
	pub file_type: String,
 | 
				
			|||
	pub key_type: String,
 | 
				
			|||
	pub name: String,
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_file_full_path(
 | 
				
			|||
	fm: &FileManager,
 | 
				
			|||
	file_type: FileType,
 | 
				
			|||
) -> Result<(String, String, PathBuf), Error> {
 | 
				
			|||
	let base_path = match file_type {
 | 
				
			|||
		FileType::Account => &fm.account_directory,
 | 
				
			|||
		FileType::PrivateKey => &fm.crt_directory,
 | 
				
			|||
		FileType::Certificate => &fm.crt_directory,
 | 
				
			|||
	};
 | 
				
			|||
	let ext = match file_type {
 | 
				
			|||
		FileType::Account => "bin".to_string(),
 | 
				
			|||
		FileType::PrivateKey => fm.pk_file_ext.clone().unwrap_or("pem".to_string()),
 | 
				
			|||
		FileType::Certificate => fm.cert_file_ext.clone().unwrap_or("pem".to_string()),
 | 
				
			|||
	};
 | 
				
			|||
	let file_name = match file_type {
 | 
				
			|||
		FileType::Account => format!(
 | 
				
			|||
			"{account}.{file_type}.{ext}",
 | 
				
			|||
			account = b64_encode(&fm.account_name),
 | 
				
			|||
			file_type = file_type,
 | 
				
			|||
			ext = ext
 | 
				
			|||
		),
 | 
				
			|||
		FileType::PrivateKey | FileType::Certificate => {
 | 
				
			|||
			let fmt_data = CertFileFormat {
 | 
				
			|||
				key_type: fm.crt_key_type.to_string(),
 | 
				
			|||
				ext,
 | 
				
			|||
				file_type: file_type.to_string(),
 | 
				
			|||
				name: fm.crt_name.to_owned(),
 | 
				
			|||
			};
 | 
				
			|||
			render_template(&fm.crt_name_format, &fmt_data)?
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	let mut path = PathBuf::from(&base_path);
 | 
				
			|||
	path.push(&file_name);
 | 
				
			|||
	Ok((base_path.to_string(), file_name, path))
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_file_path(fm: &FileManager, file_type: FileType) -> Result<PathBuf, Error> {
 | 
				
			|||
	let (_, _, path) = get_file_full_path(fm, file_type)?;
 | 
				
			|||
	Ok(path)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn read_file(fm: &FileManager, path: &Path) -> Result<Vec<u8>, Error> {
 | 
				
			|||
	fm.trace(&format!("reading file {path:?}"));
 | 
				
			|||
	let mut file = File::open(path)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
 | 
				
			|||
	let mut contents = vec![];
 | 
				
			|||
	file.read_to_end(&mut contents).await?;
 | 
				
			|||
	Ok(contents)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(unix)]
 | 
				
			|||
fn set_owner(fm: &FileManager, path: &Path, file_type: FileType) -> Result<(), Error> {
 | 
				
			|||
	let (uid, gid) = match file_type {
 | 
				
			|||
		FileType::Certificate => (fm.cert_file_owner.to_owned(), fm.cert_file_group.to_owned()),
 | 
				
			|||
		FileType::PrivateKey => (fm.pk_file_owner.to_owned(), fm.pk_file_group.to_owned()),
 | 
				
			|||
		FileType::Account => {
 | 
				
			|||
			// The account file does not need to be accessible to users other different from the current one.
 | 
				
			|||
			return Ok(());
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
	let uid = match uid {
 | 
				
			|||
		Some(u) => {
 | 
				
			|||
			if u.bytes().all(|b| b.is_ascii_digit()) {
 | 
				
			|||
				let raw_uid = u
 | 
				
			|||
					.parse::<u32>()
 | 
				
			|||
					.map_err(|_| Error::from("unable to parse the UID"))?;
 | 
				
			|||
				let nix_uid = nix::unistd::Uid::from_raw(raw_uid);
 | 
				
			|||
				Some(nix_uid)
 | 
				
			|||
			} else {
 | 
				
			|||
				let user = nix::unistd::User::from_name(&u)?;
 | 
				
			|||
				user.map(|u| u.uid)
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		None => None,
 | 
				
			|||
	};
 | 
				
			|||
	let gid = match gid {
 | 
				
			|||
		Some(g) => {
 | 
				
			|||
			if g.bytes().all(|b| b.is_ascii_digit()) {
 | 
				
			|||
				let raw_gid = g
 | 
				
			|||
					.parse::<u32>()
 | 
				
			|||
					.map_err(|_| Error::from("unable to parse the GID"))?;
 | 
				
			|||
				let nix_gid = nix::unistd::Gid::from_raw(raw_gid);
 | 
				
			|||
				Some(nix_gid)
 | 
				
			|||
			} else {
 | 
				
			|||
				let grp = nix::unistd::Group::from_name(&g)?;
 | 
				
			|||
				grp.map(|g| g.gid)
 | 
				
			|||
			}
 | 
				
			|||
		}
 | 
				
			|||
		None => None,
 | 
				
			|||
	};
 | 
				
			|||
	match uid {
 | 
				
			|||
		Some(u) => fm.trace(&format!("{path:?}: setting the uid to {}", u.as_raw())),
 | 
				
			|||
		None => fm.trace(&format!("{path:?}: uid unchanged")),
 | 
				
			|||
	};
 | 
				
			|||
	match gid {
 | 
				
			|||
		Some(g) => fm.trace(&format!("{path:?}: setting the gid to {}", g.as_raw())),
 | 
				
			|||
		None => fm.trace(&format!("{path:?}: gid unchanged")),
 | 
				
			|||
	};
 | 
				
			|||
	match nix::unistd::chown(path, uid, gid) {
 | 
				
			|||
		Ok(_) => Ok(()),
 | 
				
			|||
		Err(e) => Err(format!("{e}").into()),
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
async fn write_file(fm: &FileManager, file_type: FileType, data: &[u8]) -> Result<(), Error> {
 | 
				
			|||
	let (file_directory, file_name, path) = get_file_full_path(fm, file_type.clone())?;
 | 
				
			|||
	let mut hook_data = FileStorageHookData {
 | 
				
			|||
		file_name,
 | 
				
			|||
		file_directory,
 | 
				
			|||
		file_path: path.to_owned(),
 | 
				
			|||
		env: HashMap::new(),
 | 
				
			|||
	};
 | 
				
			|||
	hook_data.set_env(&fm.env);
 | 
				
			|||
	let is_new = !path.is_file();
 | 
				
			|||
 | 
				
			|||
	if is_new {
 | 
				
			|||
		hooks::call(fm, &fm.hooks, &hook_data, HookType::FilePreCreate).await?;
 | 
				
			|||
	} else {
 | 
				
			|||
		hooks::call(fm, &fm.hooks, &hook_data, HookType::FilePreEdit).await?;
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	fm.trace(&format!("writing file {path:?}"));
 | 
				
			|||
	let mut file = if cfg!(unix) {
 | 
				
			|||
		let mut options = OpenOptions::new();
 | 
				
			|||
		options.mode(match &file_type {
 | 
				
			|||
			FileType::Certificate => fm.cert_file_mode,
 | 
				
			|||
			FileType::PrivateKey => fm.pk_file_mode,
 | 
				
			|||
			FileType::Account => crate::DEFAULT_ACCOUNT_FILE_MODE,
 | 
				
			|||
		});
 | 
				
			|||
		options
 | 
				
			|||
			.write(true)
 | 
				
			|||
			.create(true)
 | 
				
			|||
			.open(&path)
 | 
				
			|||
			.await
 | 
				
			|||
			.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?
 | 
				
			|||
	} else {
 | 
				
			|||
		File::create(&path)
 | 
				
			|||
			.await
 | 
				
			|||
			.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?
 | 
				
			|||
	};
 | 
				
			|||
	file.write_all(data)
 | 
				
			|||
		.await
 | 
				
			|||
		.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
 | 
				
			|||
	if cfg!(unix) {
 | 
				
			|||
		set_owner(fm, &path, file_type).map_err(|e| e.prefix(&path.display().to_string()))?;
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	if is_new {
 | 
				
			|||
		hooks::call(fm, &fm.hooks, &hook_data, HookType::FilePostCreate).await?;
 | 
				
			|||
	} else {
 | 
				
			|||
		hooks::call(fm, &fm.hooks, &hook_data, HookType::FilePostEdit).await?;
 | 
				
			|||
	}
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_account_data(fm: &FileManager) -> Result<Vec<u8>, Error> {
 | 
				
			|||
	let path = get_file_path(fm, FileType::Account)?;
 | 
				
			|||
	read_file(fm, &path).await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn set_account_data(fm: &FileManager, data: &[u8]) -> Result<(), Error> {
 | 
				
			|||
	write_file(fm, FileType::Account, data).await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_keypair_path(fm: &FileManager) -> Result<PathBuf, Error> {
 | 
				
			|||
	get_file_path(fm, FileType::PrivateKey)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_keypair(fm: &FileManager) -> Result<KeyPair, Error> {
 | 
				
			|||
	let path = get_keypair_path(fm).await?;
 | 
				
			|||
	let raw_key = read_file(fm, &path).await?;
 | 
				
			|||
	let key = KeyPair::from_pem(&raw_key)?;
 | 
				
			|||
	Ok(key)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn set_keypair(fm: &FileManager, key_pair: &KeyPair) -> Result<(), Error> {
 | 
				
			|||
	let data = key_pair.private_key_to_pem()?;
 | 
				
			|||
	write_file(fm, FileType::PrivateKey, &data).await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_certificate_path(fm: &FileManager) -> Result<PathBuf, Error> {
 | 
				
			|||
	get_file_path(fm, FileType::Certificate)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn get_certificate(fm: &FileManager) -> Result<X509Certificate, Error> {
 | 
				
			|||
	let path = get_certificate_path(fm).await?;
 | 
				
			|||
	let raw_crt = read_file(fm, &path).await?;
 | 
				
			|||
	let crt = X509Certificate::from_pem(&raw_crt)?;
 | 
				
			|||
	Ok(crt)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub async fn write_certificate(fm: &FileManager, data: &[u8]) -> Result<(), Error> {
 | 
				
			|||
	write_file(fm, FileType::Certificate, data).await
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn check_files(fm: &FileManager, file_types: &[FileType]) -> bool {
 | 
				
			|||
	for t in file_types.iter().cloned() {
 | 
				
			|||
		let path = match get_file_path(fm, t) {
 | 
				
			|||
			Ok(p) => p,
 | 
				
			|||
			Err(_) => {
 | 
				
			|||
				return false;
 | 
				
			|||
			}
 | 
				
			|||
		};
 | 
				
			|||
		fm.trace(&format!(
 | 
				
			|||
			"testing file path: {}",
 | 
				
			|||
			path.to_str().unwrap_or_default()
 | 
				
			|||
		));
 | 
				
			|||
		if !path.is_file() {
 | 
				
			|||
			return false;
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
	true
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn account_files_exists(fm: &FileManager) -> bool {
 | 
				
			|||
	let file_types = vec![FileType::Account];
 | 
				
			|||
	check_files(fm, &file_types)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn certificate_files_exists(fm: &FileManager) -> bool {
 | 
				
			|||
	let file_types = vec![FileType::PrivateKey, FileType::Certificate];
 | 
				
			|||
	check_files(fm, &file_types)
 | 
				
			|||
}
 | 
				
			|||
@ -1,60 +0,0 @@ | 
				
			|||
use acme_common::error::Error;
 | 
				
			|||
use minijinja::{value::Value, Environment};
 | 
				
			|||
use serde::Serialize;
 | 
				
			|||
 | 
				
			|||
fn formatter_rev_labels(value: Value) -> Result<Value, minijinja::Error> {
 | 
				
			|||
	if let Some(value) = value.as_str() {
 | 
				
			|||
		Ok(value.rsplit('.').collect::<Vec<&str>>().join(".").into())
 | 
				
			|||
	} else {
 | 
				
			|||
		Ok(value)
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn render_template<T>(template: &str, data: &T) -> Result<String, Error>
 | 
				
			|||
where
 | 
				
			|||
	T: Serialize,
 | 
				
			|||
{
 | 
				
			|||
	let mut environment = Environment::new();
 | 
				
			|||
	environment.add_filter("rev_labels", formatter_rev_labels);
 | 
				
			|||
	environment.add_template("template", template)?;
 | 
				
			|||
	let template = environment.get_template("template")?;
 | 
				
			|||
	Ok(template.render(data)?)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
#[cfg(test)]
 | 
				
			|||
mod tests {
 | 
				
			|||
	use super::render_template;
 | 
				
			|||
	use serde::Serialize;
 | 
				
			|||
 | 
				
			|||
	#[derive(Serialize)]
 | 
				
			|||
	struct TplTest {
 | 
				
			|||
		foo: String,
 | 
				
			|||
		bar: u64,
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_basic_template() {
 | 
				
			|||
		let c = TplTest {
 | 
				
			|||
			foo: String::from("test"),
 | 
				
			|||
			bar: 42,
 | 
				
			|||
		};
 | 
				
			|||
		let tpl = "This is {{ foo }} {{ bar -}} !";
 | 
				
			|||
		let rendered = render_template(tpl, &c);
 | 
				
			|||
		assert!(rendered.is_ok());
 | 
				
			|||
		let rendered = rendered.unwrap();
 | 
				
			|||
		assert_eq!(rendered, "This is test 42!");
 | 
				
			|||
	}
 | 
				
			|||
 | 
				
			|||
	#[test]
 | 
				
			|||
	fn test_formatter_rev_labels() {
 | 
				
			|||
		let c = TplTest {
 | 
				
			|||
			foo: String::from("mx1.example.org"),
 | 
				
			|||
			bar: 42,
 | 
				
			|||
		};
 | 
				
			|||
		let tpl = "{{ foo }} - {{ foo | rev_labels }}";
 | 
				
			|||
		let rendered = render_template(tpl, &c);
 | 
				
			|||
		assert!(rendered.is_ok());
 | 
				
			|||
		let rendered = rendered.unwrap();
 | 
				
			|||
		assert_eq!(rendered, "mx1.example.org - org.example.mx1");
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
@ -0,0 +1 @@ | 
				
			|||
fn main() {}
 | 
				
			|||
@ -1,28 +0,0 @@ | 
				
			|||
[package] | 
				
			|||
name = "tacd" | 
				
			|||
version = "0.24.0" | 
				
			|||
authors = ["Rodolphe Breard <rodolphe@what.tf>"] | 
				
			|||
edition = "2018" | 
				
			|||
description = "TLS-ALPN Challenge Daemon" | 
				
			|||
readme = "../README.md" | 
				
			|||
repository = "https://github.com/breard-r/acmed" | 
				
			|||
license = "MIT OR Apache-2.0" | 
				
			|||
keywords = ["acme", "tls", "alpn", "X.509"] | 
				
			|||
categories = ["cryptography"] | 
				
			|||
include = ["src/**/*", "Cargo.toml", "LICENSE-*.txt"] | 
				
			|||
publish = false | 
				
			|||
rust-version = "1.74.0" | 
				
			|||
 | 
				
			|||
[features] | 
				
			|||
default = ["openssl_dyn"] | 
				
			|||
crypto_openssl = [] | 
				
			|||
openssl_dyn = ["crypto_openssl", "acme_common/openssl_dyn"] | 
				
			|||
openssl_vendored = ["crypto_openssl", "acme_common/openssl_vendored"] | 
				
			|||
 | 
				
			|||
[dependencies] | 
				
			|||
acme_common = { path = "../acme_common" } | 
				
			|||
anyhow = "1.0.81" | 
				
			|||
clap = { version = "4.5.3", features = ["string"] } | 
				
			|||
log = "0.4.21" | 
				
			|||
openssl = "0.10.64" | 
				
			|||
thiserror = "2.0.3" | 
				
			|||
@ -1,40 +0,0 @@ | 
				
			|||
use std::env;
 | 
				
			|||
use std::path::PathBuf;
 | 
				
			|||
 | 
				
			|||
macro_rules! set_rustc_env_var {
 | 
				
			|||
	($name: expr, $value: expr) => {{
 | 
				
			|||
		println!("cargo:rustc-env={}={}", $name, $value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_env_var_if_absent {
 | 
				
			|||
	($name: expr, $default_value: expr) => {{
 | 
				
			|||
		if let Err(_) = env::var($name) {
 | 
				
			|||
			set_rustc_env_var!($name, $default_value);
 | 
				
			|||
		}
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_specific_path_if_absent {
 | 
				
			|||
	($env_name: expr, $env_default: expr, $name: expr, $default_value: expr) => {{
 | 
				
			|||
		let prefix = env::var($env_name).unwrap_or(String::from($env_default));
 | 
				
			|||
		let mut value = PathBuf::new();
 | 
				
			|||
		value.push(prefix);
 | 
				
			|||
		value.push($default_value);
 | 
				
			|||
		set_env_var_if_absent!($name, value.to_str().unwrap());
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
macro_rules! set_runstate_path_if_absent {
 | 
				
			|||
	($name: expr, $default_value: expr) => {{
 | 
				
			|||
		set_specific_path_if_absent!("RUNSTATEDIR", "/run", $name, $default_value);
 | 
				
			|||
	}};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn main() {
 | 
				
			|||
	if let Ok(target) = env::var("TARGET") {
 | 
				
			|||
		println!("cargo:rustc-env=TACD_TARGET={target}");
 | 
				
			|||
	};
 | 
				
			|||
 | 
				
			|||
	set_runstate_path_if_absent!("TACD_DEFAULT_PID_FILE", "tacd.pid");
 | 
				
			|||
}
 | 
				
			|||
@ -1,224 +0,0 @@ | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
mod openssl_server;
 | 
				
			|||
 | 
				
			|||
#[cfg(feature = "crypto_openssl")]
 | 
				
			|||
use crate::openssl_server::start as server_start;
 | 
				
			|||
use acme_common::crypto::{get_lib_name, get_lib_version, HashFunction, KeyType, X509Certificate};
 | 
				
			|||
use acme_common::logs::{set_log_system, DEFAULT_LOG_LEVEL};
 | 
				
			|||
use acme_common::{clean_pid_file, to_idna};
 | 
				
			|||
use anyhow::{anyhow, Result};
 | 
				
			|||
use clap::builder::PossibleValuesParser;
 | 
				
			|||
use clap::{Arg, ArgAction, ArgMatches, Command};
 | 
				
			|||
use log::{debug, error, info};
 | 
				
			|||
use std::fs::File;
 | 
				
			|||
use std::io::{self, Read};
 | 
				
			|||
 | 
				
			|||
const APP_NAME: &str = env!("CARGO_PKG_NAME");
 | 
				
			|||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
 | 
				
			|||
const DEFAULT_PID_FILE: &str = env!("TACD_DEFAULT_PID_FILE");
 | 
				
			|||
const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:5001";
 | 
				
			|||
const DEFAULT_CRT_KEY_TYPE: KeyType = KeyType::EcdsaP256;
 | 
				
			|||
const DEFAULT_CRT_DIGEST: HashFunction = HashFunction::Sha256;
 | 
				
			|||
const ALPN_ACME_PROTO_NAME: &[u8] = b"\x0aacme-tls/1";
 | 
				
			|||
 | 
				
			|||
fn read_line(path: Option<&String>) -> Result<String> {
 | 
				
			|||
	let mut input = String::new();
 | 
				
			|||
	match path {
 | 
				
			|||
		Some(p) => File::open(p)?.read_to_string(&mut input)?,
 | 
				
			|||
		None => io::stdin().read_line(&mut input)?,
 | 
				
			|||
	};
 | 
				
			|||
	let line = input.trim().to_string();
 | 
				
			|||
	Ok(line)
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn get_acme_value(cnf: &ArgMatches, opt: &str, opt_file: &str) -> Result<String> {
 | 
				
			|||
	match cnf.get_one::<String>(opt) {
 | 
				
			|||
		Some(v) => Ok(v.to_string()),
 | 
				
			|||
		None => {
 | 
				
			|||
			debug!(
 | 
				
			|||
				"reading {opt} from {}",
 | 
				
			|||
				cnf.get_one::<String>(opt_file)
 | 
				
			|||
					.map(|e| e.as_str())
 | 
				
			|||
					.unwrap_or("stdin")
 | 
				
			|||
			);
 | 
				
			|||
			read_line(cnf.get_one::<String>(opt_file))
 | 
				
			|||
		}
 | 
				
			|||
	}
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn init(cnf: &ArgMatches) -> Result<()> {
 | 
				
			|||
	acme_common::init_server(
 | 
				
			|||
		cnf.get_flag("foreground"),
 | 
				
			|||
		cnf.get_one::<String>("pid-file").map(|e| e.as_str()),
 | 
				
			|||
	);
 | 
				
			|||
	let domain = get_acme_value(cnf, "domain", "domain-file")?;
 | 
				
			|||
	let domain = to_idna(&domain).map_err(|e| anyhow!(e))?;
 | 
				
			|||
	let ext = get_acme_value(cnf, "acme-ext", "acme-ext-file")?;
 | 
				
			|||
	let listen_addr = cnf
 | 
				
			|||
		.get_one::<String>("listen")
 | 
				
			|||
		.map(|e| e.as_str())
 | 
				
			|||
		.unwrap_or(DEFAULT_LISTEN_ADDR);
 | 
				
			|||
	let crt_signature_alg = match cnf.get_one::<&str>("crt-signature-alg") {
 | 
				
			|||
		Some(alg) => alg
 | 
				
			|||
			.parse()
 | 
				
			|||
			.map_err(|e: acme_common::error::Error| anyhow!(e))?,
 | 
				
			|||
		None => DEFAULT_CRT_KEY_TYPE,
 | 
				
			|||
	};
 | 
				
			|||
	let crt_digest = match cnf.get_one::<&str>("crt-digest") {
 | 
				
			|||
		Some(alg) => alg
 | 
				
			|||
			.parse()
 | 
				
			|||
			.map_err(|e: acme_common::error::Error| anyhow!(e))?,
 | 
				
			|||
		None => DEFAULT_CRT_DIGEST,
 | 
				
			|||
	};
 | 
				
			|||
	let (pk, cert) = X509Certificate::from_acme_ext(&domain, &ext, crt_signature_alg, crt_digest)
 | 
				
			|||
		.map_err(|e| anyhow!(e))?;
 | 
				
			|||
	info!("starting {APP_NAME} on {listen_addr} for {domain}");
 | 
				
			|||
	server_start(listen_addr, &cert, &pk)?;
 | 
				
			|||
	Ok(())
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
fn main() {
 | 
				
			|||
	let full_version = format!(
 | 
				
			|||
		"{APP_VERSION} built for {}\n\nCryptographic library:\n - {} {}",
 | 
				
			|||
		env!("TACD_TARGET"),
 | 
				
			|||
		get_lib_name(),
 | 
				
			|||
		get_lib_version(),
 | 
				
			|||
	);
 | 
				
			|||
	let default_crt_key_type = DEFAULT_CRT_KEY_TYPE.to_string();
 | 
				
			|||
	let default_crt_digest = DEFAULT_CRT_DIGEST.to_string();
 | 
				
			|||
	let default_log_level = DEFAULT_LOG_LEVEL.to_string().to_lowercase();
 | 
				
			|||
	let matches = Command::new(APP_NAME)
 | 
				
			|||
		.version(APP_VERSION)
 | 
				
			|||
		.long_version(full_version)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("listen")
 | 
				
			|||
				.long("listen")
 | 
				
			|||
				.short('l')
 | 
				
			|||
				.help("Host and port to listen on")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("host:port|unix:path")
 | 
				
			|||
				.default_value(DEFAULT_LISTEN_ADDR),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("domain")
 | 
				
			|||
				.long("domain")
 | 
				
			|||
				.short('d')
 | 
				
			|||
				.help("The domain that is being validated")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("STRING")
 | 
				
			|||
				.conflicts_with("domain-file"),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("domain-file")
 | 
				
			|||
				.long("domain-file")
 | 
				
			|||
				.help("File from which is read the domain that is being validated")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("FILE")
 | 
				
			|||
				.conflicts_with("domain"),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("acme-ext")
 | 
				
			|||
				.long("acme-ext")
 | 
				
			|||
				.short('e')
 | 
				
			|||
				.help("The acmeIdentifier extension to set in the self-signed certificate")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("STRING")
 | 
				
			|||
				.conflicts_with("acme-ext-file"),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("acme-ext-file")
 | 
				
			|||
				.long("acme-ext-file")
 | 
				
			|||
				.help("File from which is read the acmeIdentifier extension to set in the self-signed certificate")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("FILE")
 | 
				
			|||
				.conflicts_with("acme-ext"),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("crt-signature-alg")
 | 
				
			|||
				.long("crt-signature-alg")
 | 
				
			|||
				.help("The certificate's signature algorithm")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("STRING")
 | 
				
			|||
				.value_parser(PossibleValuesParser::new(KeyType::list_possible_values()))
 | 
				
			|||
				.default_value(default_crt_key_type),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("crt-digest")
 | 
				
			|||
				.long("crt-digest")
 | 
				
			|||
				.help("The certificate's digest algorithm")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("STRING")
 | 
				
			|||
				.value_parser(PossibleValuesParser::new(HashFunction::list_possible_values()))
 | 
				
			|||
				.default_value(default_crt_digest),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("log-level")
 | 
				
			|||
				.long("log-level")
 | 
				
			|||
				.help("Specify the log level")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("LEVEL")
 | 
				
			|||
				.value_parser(["error", "warn", "info", "debug", "trace"])
 | 
				
			|||
				.default_value(default_log_level),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("to-syslog")
 | 
				
			|||
				.long("log-syslog")
 | 
				
			|||
				.help("Sends log messages via syslog")
 | 
				
			|||
				.conflicts_with("to-stderr")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("to-stderr")
 | 
				
			|||
				.long("log-stderr")
 | 
				
			|||
				.help("Prints log messages to the standard error output")
 | 
				
			|||
				.conflicts_with("to-syslog")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("foreground")
 | 
				
			|||
				.long("foreground")
 | 
				
			|||
				.short('f')
 | 
				
			|||
				.help("Runs in the foreground")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("pid-file")
 | 
				
			|||
				.long("pid-file")
 | 
				
			|||
				.help("Path to the PID file")
 | 
				
			|||
				.num_args(1)
 | 
				
			|||
				.value_name("FILE")
 | 
				
			|||
				.default_value(DEFAULT_PID_FILE)
 | 
				
			|||
				.default_value_if("no-pid-file", clap::builder::ArgPredicate::IsPresent, None)
 | 
				
			|||
				.conflicts_with("no-pid-file"),
 | 
				
			|||
		)
 | 
				
			|||
		.arg(
 | 
				
			|||
			Arg::new("no-pid-file")
 | 
				
			|||
				.long("no-pid-file")
 | 
				
			|||
				.help("Do not create any PID file")
 | 
				
			|||
				.conflicts_with("pid-file")
 | 
				
			|||
				.action(ArgAction::SetTrue),
 | 
				
			|||
		)
 | 
				
			|||
		.get_matches();
 | 
				
			|||
 | 
				
			|||
	match set_log_system(
 | 
				
			|||
		matches.get_one::<String>("log-level").map(|e| e.as_str()),
 | 
				
			|||
		matches.get_flag("to-syslog"),
 | 
				
			|||
		matches.get_flag("to-stderr"),
 | 
				
			|||
	) {
 | 
				
			|||
		Ok(_) => {}
 | 
				
			|||
		Err(e) => {
 | 
				
			|||
			eprintln!("Error: {e}");
 | 
				
			|||
			std::process::exit(2);
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
 | 
				
			|||
	match init(&matches) {
 | 
				
			|||
		Ok(_) => {}
 | 
				
			|||
		Err(e) => {
 | 
				
			|||
			error!("{e}");
 | 
				
			|||
			let pid_file = matches.get_one::<String>("pid-file").map(|e| e.as_str());
 | 
				
			|||
			let _ = clean_pid_file(pid_file);
 | 
				
			|||
			std::process::exit(1);
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
@ -1,48 +0,0 @@ | 
				
			|||
use acme_common::crypto::{KeyPair, X509Certificate};
 | 
				
			|||
use anyhow::{bail, Result};
 | 
				
			|||
use log::debug;
 | 
				
			|||
use openssl::ssl::{self, AlpnError, SslAcceptor, SslMethod};
 | 
				
			|||
use std::net::TcpListener;
 | 
				
			|||
use std::sync::Arc;
 | 
				
			|||
use std::thread;
 | 
				
			|||
 | 
				
			|||
#[cfg(target_family = "unix")]
 | 
				
			|||
use std::os::unix::net::UnixListener;
 | 
				
			|||
 | 
				
			|||
const ALPN_ERROR: AlpnError = AlpnError::ALERT_FATAL;
 | 
				
			|||
 | 
				
			|||
macro_rules! listen_and_accept {
 | 
				
			|||
	($lt: ident, $addr: ident, $acceptor: ident) => {
 | 
				
			|||
		let listener = $lt::bind($addr)?;
 | 
				
			|||
		for stream in listener.incoming() {
 | 
				
			|||
			if let Ok(stream) = stream {
 | 
				
			|||
				let acceptor = $acceptor.clone();
 | 
				
			|||
				thread::spawn(move || {
 | 
				
			|||
					debug!("new client");
 | 
				
			|||
					let _ = acceptor.accept(stream).unwrap();
 | 
				
			|||
				});
 | 
				
			|||
			};
 | 
				
			|||
		}
 | 
				
			|||
	};
 | 
				
			|||
}
 | 
				
			|||
 | 
				
			|||
pub fn start(listen_addr: &str, certificate: &X509Certificate, key_pair: &KeyPair) -> Result<()> {
 | 
				
			|||
	let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
 | 
				
			|||
	acceptor.set_alpn_select_callback(|_, client| {
 | 
				
			|||
		debug!("ALPN negociation");
 | 
				
			|||
		ssl::select_next_proto(crate::ALPN_ACME_PROTO_NAME, client).ok_or(ALPN_ERROR)
 | 
				
			|||
	});
 | 
				
			|||
	acceptor.set_private_key(&key_pair.inner_key)?;
 | 
				
			|||
	acceptor.set_certificate(&certificate.inner_cert)?;
 | 
				
			|||
	acceptor.check_private_key()?;
 | 
				
			|||
	let acceptor = Arc::new(acceptor.build());
 | 
				
			|||
	if cfg!(unix) && listen_addr.starts_with("unix:") {
 | 
				
			|||
		let listen_addr = &listen_addr[5..];
 | 
				
			|||
		debug!("listening on unix socket {listen_addr}");
 | 
				
			|||
		listen_and_accept!(UnixListener, listen_addr, acceptor);
 | 
				
			|||
	} else {
 | 
				
			|||
		debug!("listening on {listen_addr}");
 | 
				
			|||
		listen_and_accept!(TcpListener, listen_addr, acceptor);
 | 
				
			|||
	}
 | 
				
			|||
	bail!("main thread loop unexpectedly exited")
 | 
				
			|||
}
 | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue