Browse Source

Great reset

ng
Rodolphe Bréard 1 month ago
parent
commit
7b4d15cfea
Failed to extract signature
  1. 2297
      Cargo.lock
  2. 42
      Cargo.toml
  3. 24
      Makefile
  4. 41
      acme_common/Cargo.toml
  5. 23
      acme_common/build.rs
  6. 93
      acme_common/src/crypto.rs
  7. 81
      acme_common/src/crypto/jws_signature_algorithm.rs
  8. 103
      acme_common/src/crypto/key_type.rs
  9. 203
      acme_common/src/crypto/openssl_certificate.rs
  10. 34
      acme_common/src/crypto/openssl_hash.rs
  11. 343
      acme_common/src/crypto/openssl_keys.rs
  12. 25
      acme_common/src/crypto/openssl_subject_attribute.rs
  13. 27
      acme_common/src/crypto/openssl_version.rs
  14. 133
      acme_common/src/error.rs
  15. 76
      acme_common/src/lib.rs
  16. 86
      acme_common/src/logs.rs
  17. 5
      acme_common/src/tests.rs
  18. 181
      acme_common/src/tests/certificate.rs
  19. 411
      acme_common/src/tests/crypto_keys.rs
  20. 344
      acme_common/src/tests/hash.rs
  21. 42
      acme_common/src/tests/idna.rs
  22. 62
      acme_common/src/tests/jws_signature_algorithm.rs
  23. 46
      acmed/Cargo.toml
  24. 130
      acmed/build.rs
  25. 319
      acmed/src/account.rs
  26. 110
      acmed/src/account/contact.rs
  27. 186
      acmed/src/account/storage.rs
  28. 292
      acmed/src/acme_proto.rs
  29. 154
      acmed/src/acme_proto/account.rs
  30. 25
      acmed/src/acme_proto/certificate.rs
  31. 149
      acmed/src/acme_proto/http.rs
  32. 29
      acmed/src/acme_proto/structs.rs
  33. 202
      acmed/src/acme_proto/structs/account.rs
  34. 349
      acmed/src/acme_proto/structs/authorization.rs
  35. 151
      acmed/src/acme_proto/structs/directory.rs
  36. 173
      acmed/src/acme_proto/structs/error.rs
  37. 135
      acmed/src/acme_proto/structs/order.rs
  38. 203
      acmed/src/certificate.rs
  39. 797
      acmed/src/config.rs
  40. 51
      acmed/src/duration.rs
  41. 130
      acmed/src/endpoint.rs
  42. 215
      acmed/src/hooks.rs
  43. 288
      acmed/src/http.rs
  44. 147
      acmed/src/identifier.rs
  45. 191
      acmed/src/jws.rs
  46. 6
      acmed/src/logs.rs
  47. 180
      acmed/src/main.rs
  48. 223
      acmed/src/main_event_loop.rs
  49. 312
      acmed/src/storage.rs
  50. 60
      acmed/src/template.rs
  51. 0
      config/acmed.toml
  52. 0
      config/default_hooks.toml
  53. 0
      config/letsencrypt.toml
  54. 18
      release.sh
  55. 1
      src/main.rs
  56. 28
      tacd/Cargo.toml
  57. 40
      tacd/build.rs
  58. 224
      tacd/src/main.rs
  59. 48
      tacd/src/openssl_server.rs

2297
Cargo.lock
File diff suppressed because it is too large
View File

42
Cargo.toml

@ -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

24
Makefile

@ -14,27 +14,15 @@ MAN_DST_DIR = $(TARGET_DIR)/man
FEATURES = openssl_dyn
all: acmed tacd
acmed: man_dir
all: man_dir
if test -n "$(TARGET)"; then \
VARLIBDIR="$(VARLIBDIR)" SYSCONFDIR="$(SYSCONFDIR)" RUNSTATEDIR="$(RUNSTATEDIR)" cargo build --bin acmed --release --no-default-features --features "$(FEATURES)" --target "$(TARGET)"; \
VARLIBDIR="$(VARLIBDIR)" SYSCONFDIR="$(SYSCONFDIR)" RUNSTATEDIR="$(RUNSTATEDIR)" cargo build --release --no-default-features --features "$(FEATURES)" --target "$(TARGET)"; \
else \
VARLIBDIR="$(VARLIBDIR)" SYSCONFDIR="$(SYSCONFDIR)" RUNSTATEDIR="$(RUNSTATEDIR)" cargo build --bin acmed --release --no-default-features --features "$(FEATURES)"; \
VARLIBDIR="$(VARLIBDIR)" SYSCONFDIR="$(SYSCONFDIR)" RUNSTATEDIR="$(RUNSTATEDIR)" cargo build --release --no-default-features --features "$(FEATURES)"; \
fi
strip "$(TARGET_DIR)/acmed"
gzip <"$(MAN_SRC_DIR)/acmed.8" >"$(MAN_DST_DIR)/acmed.8.gz"
gzip <"$(MAN_SRC_DIR)/acmed.toml.5" >"$(MAN_DST_DIR)/acmed.toml.5.gz"
tacd: man_dir
if test -n "$(TARGET)"; then \
VARLIBDIR="$(VARLIBDIR)" SYSCONFDIR="$(SYSCONFDIR)" RUNSTATEDIR="$(RUNSTATEDIR)" cargo build --bin tacd --release --no-default-features --features "$(FEATURES)" --target "$(TARGET)"; \
else \
VARLIBDIR="$(VARLIBDIR)" SYSCONFDIR="$(SYSCONFDIR)" RUNSTATEDIR="$(RUNSTATEDIR)" cargo build --bin tacd --release --no-default-features --features "$(FEATURES)"; \
fi
strip "$(TARGET_DIR)/tacd"
gzip <"$(MAN_SRC_DIR)/tacd.8" >"$(MAN_DST_DIR)/tacd.8.gz"
man_dir:
@mkdir -p $(MAN_DST_DIR)
@ -53,12 +41,8 @@ install:
install -m 0644 acmed/config/default_hooks.toml $(DESTDIR)$(SYSCONFDIR)/acmed/default_hooks.toml; \
install -m 0644 acmed/config/letsencrypt.toml $(DESTDIR)$(SYSCONFDIR)/acmed/letsencrypt.toml; \
fi
if test -f "$(TARGET_DIR)/tacd"; then \
install -m 0755 $(TARGET_DIR)/tacd $(DESTDIR)$(BINDIR)/tacd; \
install -m 0644 $(TARGET_DIR)/man/tacd.8.gz $(DESTDIR)$(MAN8DIR)/tacd.8.gz; \
fi
clean:
cargo clean
.PHONY: all acmed tacd man_dir install clean
.PHONY: all man_dir install clean

41
acme_common/Cargo.toml

@ -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"

23
acme_common/build.rs

@ -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");
}
}

93
acme_common/src/crypto.rs

@ -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};

81
acme_common/src/crypto/jws_signature_algorithm.rs

@ -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");
}
}

103
acme_common/src/crypto/key_type.rs

@ -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}")
}
}

203
acme_common/src/crypto/openssl_certificate.rs

@ -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(&not_before)?;
let not_after = Asn1Time::days_from_now(super::CRT_NB_DAYS_VALIDITY)?;
builder.set_not_after(&not_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)
}

34
acme_common/src/crypto/openssl_hash.rs

@ -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(),
}
}
}

343
acme_common/src/crypto/openssl_keys.rs

@ -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)
}

25
acme_common/src/crypto/openssl_subject_attribute.rs

@ -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,
}
}
}

27
acme_common/src/crypto/openssl_version.rs

@ -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)
}

133
acme_common/src/error.rs

@ -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()
}
}

76
acme_common/src/lib.rs

@ -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(())
}

86
acme_common/src/logs.rs

@ -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);
}
}

5
acme_common/src/tests.rs

@ -1,5 +0,0 @@
mod certificate;
mod crypto_keys;
mod hash;
mod idna;
mod jws_signature_algorithm;

181
acme_common/src/tests/certificate.rs

@ -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);
}

411
acme_common/src/tests/crypto_keys.rs

@ -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"
);
}

344
acme_common/src/tests/hash.rs

@ -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);
}
}

42
acme_common/src/tests/idna.rs

@ -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"
);
}

62
acme_common/src/tests/jws_signature_algorithm.rs

@ -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();
}

46
acmed/Cargo.toml

@ -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"

130
acmed/build.rs

@ -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();
}

319
acmed/src/account.rs

@ -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)
}

110
acmed/src/account/contact.rs

@ -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));
}
}

186
acmed/src/account/storage.rs

@ -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())
}

292
acmed/src/acme_proto.rs

@ -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(())
}

154
acmed/src/acme_proto/account.rs

@ -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(())
}

25
acmed/src/acme_proto/certificate.rs

@ -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
}
}

149
acmed/src/acme_proto/http.rs

@ -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)
}

29
acmed/src/acme_proto/structs.rs

@ -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};

202
acmed/src/acme_proto/structs/account.rs

@ -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\""));
}
}

349
acmed/src/acme_proto/structs/authorization.rs

@ -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),
}
}
}

151
acmed/src/acme_proto/structs/directory.rs

@ -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());
}
}

173
acmed/src/acme_proto/structs/error.rs

@ -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()
}
}

135
acmed/src/acme_proto/structs/order.rs

@ -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());
}
}

203
acmed/src/certificate.rs

@ -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(())
}
}

797
acmed/src/config.rs

@ -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)
}

51
acmed/src/duration.rs

@ -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()),
}
}

130
acmed/src/endpoint.rs

@ -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);
}
}
}
}

215
acmed/src/hooks.rs

@ -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(())
}

288
acmed/src/http.rs

@ -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));
}
}
}

147
acmed/src/identifier.rs

@ -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"
);
}
}

191
acmed/src/jws.rs

@ -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));
}
}

6
acmed/src/logs.rs

@ -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);
}

180
acmed/src/main.rs

@ -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;
}

223
acmed/src/main_event_loop.rs

@ -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())
}

312
acmed/src/storage.rs

@ -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)
}

60
acmed/src/template.rs

@ -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
acmed/config/acmed.toml → config/acmed.toml

0
acmed/config/default_hooks.toml → config/default_hooks.toml

0
acmed/config/letsencrypt.toml → config/letsencrypt.toml

18
release.sh

@ -12,19 +12,17 @@ abort_release()
display_crate_version()
{
local crate_name="$1"
local crate_version
crate_version=$(grep "^version" "${crate_name}/Cargo.toml" | cut -d '"' -f2)
echo "Current version for crate ${crate_name}: ${crate_version}"
crate_version=$(grep "^version" "Cargo.toml" | cut -d '"' -f2)
echo "Current version: ${crate_version}"
}
update_crate_version()
{
local crate_name="$1"
local new_version="$2"
sed -i "s/^version = .*/version = \"${new_version}\"/" "${crate_name}/Cargo.toml"
sed -i "s/^version = .*/version = \"${new_version}\"/" "/Cargo.toml"
}
display_man_date()
@ -84,13 +82,10 @@ release_new_version()
local current_date="$2"
local confirm_git_diff
update_crate_version "acme_common" "${new_version}"
update_crate_version "acmed" "${new_version}"
update_crate_version "tacd" "${new_version}"
update_crate_version "${new_version}"
update_man_date "acmed.8" "${current_date}"
update_man_date "acmed.toml.5" "${current_date}"
update_man_date "tacd.8" "${current_date}"
update_changelog "${new_version}"
@ -118,14 +113,11 @@ main()
check_working_directory
display_crate_version "acme_common"
display_crate_version "acmed"
display_crate_version "tacd"
display_crate_version
echo
display_man_date "acmed.8"
display_man_date "acmed.toml.5"
display_man_date "tacd.8"
echo
echo -n "Enter the new version: "

1
src/main.rs

@ -0,0 +1 @@
fn main() {}

28
tacd/Cargo.toml

@ -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"

40
tacd/build.rs

@ -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");
}

224
tacd/src/main.rs

@ -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);
}
};
}

48
tacd/src/openssl_server.rs

@ -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")
}
Loading…
Cancel
Save