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