Compare commits
29 Commits
d85ac9416e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
76334e3fa4
|
|||
|
a1c7bcba30
|
|||
|
b5c142838f
|
|||
|
87e052e67b
|
|||
|
2019fde054
|
|||
|
cb47c5c4e6
|
|||
|
d36335a885
|
|||
|
9636eba5f7
|
|||
|
732ad63dbe
|
|||
|
edce2cdcd5
|
|||
|
b6267a0c76
|
|||
|
8135d1ceed
|
|||
|
09291d553c
|
|||
|
7b768c9302
|
|||
|
315fddaa15
|
|||
|
10d680a364
|
|||
|
9e3ae38447
|
|||
|
31e480a7b2
|
|||
|
5ebe0a2dae
|
|||
|
835373dd12
|
|||
|
0a51e5de08
|
|||
|
8c8173de79
|
|||
|
f655c6a87f
|
|||
|
41f81ebdd9
|
|||
|
3564e6738c
|
|||
|
4a00f96763
|
|||
|
6639276e11
|
|||
|
ac540437dd
|
|||
|
8282a4c704
|
3539
Cargo.lock
generated
3539
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "acme-client"
|
name = "acme-client"
|
||||||
version = "1.1.0"
|
version = "1.3.9"
|
||||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Acme auto challenge client, acme-client can issue certificates from Let's encrypt"
|
description = "Acme auto challenge client, acme-client can issue certificates from Let's encrypt"
|
||||||
@@ -11,21 +11,22 @@ description = "Acme auto challenge client, acme-client can issue certificates fr
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
rust_util = "0.6"
|
rust_util = "0.6"
|
||||||
acme-lib = "0.8"
|
acme-lib = "0.9"
|
||||||
tide = "0.16"
|
tide = "0.16"
|
||||||
async-std = { version = "1.8", features = ["attributes"] }
|
async-std = { version = "1.8", features = ["attributes"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
deser-hjson = "0.1"
|
deser-hjson = "2.2"
|
||||||
x509-parser = "0.9"
|
x509-parser = "0.9"
|
||||||
reqwest = { version = "0.11", features = ["blocking"] }
|
reqwest = { version = "0.11", default-features = false, features = ["blocking", "native-tls-vendored"] }
|
||||||
trust-dns-resolver = "0.20"
|
#reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||||
|
trust-dns-resolver = "0.23"
|
||||||
simpledateformat = "0.1.3"
|
simpledateformat = "0.1.3"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
urlencoding = "1.0.0"
|
urlencoding = "2.1"
|
||||||
base64 = "0.11.0"
|
base64 = "0.21"
|
||||||
hmac = "0.7.1"
|
hmac = "0.12"
|
||||||
sha2 = "0.8.1"
|
sha2 = "0.10"
|
||||||
aliyun-openapi-core-rust-sdk = "0.3.0"
|
aliyun-openapi-core-rust-sdk = "1.1.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -5,7 +5,7 @@ ACME Client in Rust
|
|||||||
Acme client help:
|
Acme client help:
|
||||||
```shell
|
```shell
|
||||||
$ acme-client --help
|
$ acme-client --help
|
||||||
acme-client 1.0.1
|
acme-client 1.1.0
|
||||||
Hatter Jiang <jht5945@gmail.com>
|
Hatter Jiang <jht5945@gmail.com>
|
||||||
Acme auto challenge client, acme-client can issue certificates from Let's encrypt
|
Acme auto challenge client, acme-client can issue certificates from Let's encrypt
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ USAGE:
|
|||||||
acme-client [FLAGS] [OPTIONS]
|
acme-client [FLAGS] [OPTIONS]
|
||||||
|
|
||||||
FLAGS:
|
FLAGS:
|
||||||
|
--allow-interact Allow interact
|
||||||
--check Check cert config
|
--check Check cert config
|
||||||
-h, --help Prints help information
|
-h, --help Prints help information
|
||||||
--hide-logo Hide logo
|
--hide-logo Hide logo
|
||||||
@@ -29,6 +30,7 @@ OPTIONS:
|
|||||||
-d, --domain <domain>... Domains
|
-d, --domain <domain>... Domains
|
||||||
--email <email> Contract email
|
--email <email> Contract email
|
||||||
-m, --mode <mode> Mode [default: prod]
|
-m, --mode <mode> Mode [default: prod]
|
||||||
|
-o, --outputs <outputs> Outputs file
|
||||||
-p, --port <port> Http port [default: 80]
|
-p, --port <port> Http port [default: 80]
|
||||||
--timeout <timeout> Timeout (ms) [default: 5000]
|
--timeout <timeout> Timeout (ms) [default: 5000]
|
||||||
-t, --type <type> Type http or dns [default: http]
|
-t, --type <type> Type http or dns [default: http]
|
||||||
@@ -42,11 +44,19 @@ OPTIONS:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"port": 18342,
|
"port": 18342,
|
||||||
|
"credentialSuppliers": {
|
||||||
|
"alibabacloud": "account://access_key_id:access_key_secret@alibabacloud?id=dns"
|
||||||
|
},
|
||||||
"triggerAfterUpdate": ["/usr/local/nginx/nginx", "-s", "reload"],
|
"triggerAfterUpdate": ["/usr/local/nginx/nginx", "-s", "reload"],
|
||||||
"notifyToken": "dingtalk:access_token?sec_token",
|
"notifyToken": "dingtalk:access_token?sec_token",
|
||||||
"certItems": [{
|
"certItems": [{
|
||||||
"path": "dir_cryptofan_org",
|
"path": "dir_cryptofan_org",
|
||||||
"dnsNames": ["cryptofan.org", "www.cryptofan.org"]
|
"dnsNames": ["cryptofan.org", "www.cryptofan.org"]
|
||||||
|
}, {
|
||||||
|
"path": "dir_webauthn_host",
|
||||||
|
"dnsNames": ["webauthn.host", "*.webauthn.host"],
|
||||||
|
"type": "dns",
|
||||||
|
"supplier": "alibabacloud"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -59,8 +69,23 @@ location /.well-known/acme-challenge/ {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
通过命令行交互创建DNS挑战证书:
|
||||||
|
```shell
|
||||||
|
acme-client --port 0 -t dns --allow-interact --email email@example.com -d example.net
|
||||||
|
```
|
||||||
|
|
||||||
|
* `email@example.com` -- your email
|
||||||
|
* `example.net` -- your domain
|
||||||
|
|
||||||
|
出现以下提示时需要自行配置DNS,配置完成后按"回车":
|
||||||
|
```shell
|
||||||
|
[INFO ] You need to config dns manually, press enter to continue...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Cross build uses: https://github.com/messense/rust-musl-cross
|
Cross build uses:
|
||||||
|
- ~~https://github.com/messense/rust-musl-cross~~
|
||||||
|
- https://github.com/emk/rust-musl-builder
|
||||||
|
|
||||||
|
|||||||
21
justfile
21
justfile
@@ -1,23 +1,6 @@
|
|||||||
_:
|
_:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
## cross build x86-64 musl debug
|
build-linux-x64-musl:
|
||||||
#cross-build-x64-debug:
|
cargo zigbuild --release --target x86_64-unknown-linux-musl
|
||||||
# cross build --target x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
## cross build x86-64 musl release
|
|
||||||
#cross-build-x64:
|
|
||||||
# cross build --target x86_64-unknown-linux-musl --release
|
|
||||||
|
|
||||||
# cross build x86-64 musl release(for openssl)
|
|
||||||
cross-build-x64-musl:
|
|
||||||
docker run --rm -it -v "$(echo $HOME)/.cargo_messense_rust-musl-cross_x86_64-musl_registry":/root/.cargo/registry/ -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release
|
|
||||||
|
|
||||||
# cross build x86-64 musl release(for openssl) 2
|
|
||||||
cross-build-x64-musl-2:
|
|
||||||
docker run --rm -it -v "$(echo $HOME)/.cargo/config":/root/.cargo/config -v "$(echo $HOME)/.cargo_messense_rust-musl-cross_x86_64-musl_registry":/root/.cargo/registry/ -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release
|
|
||||||
|
|
||||||
# cross build x86-64 musl release(for openssl) 3
|
|
||||||
cross-build-x64-musl-3:
|
|
||||||
docker run --rm -it -v "$(echo $HOME)/.cargo/config":/root/.cargo/config -v "$(echo $HOME)/.cargo_messense_rust-musl-cross_x86_64-musl_registry":/root/.cargo/registry/ -v "$(pwd)":/volume clux/muslrust cargo build --release --target=x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
|
|||||||
206
src/acme.rs
Normal file
206
src/acme.rs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use acme_lib::{Directory, create_p256_key, create_p384_key, create_rsa_key, DirectoryUrl};
|
||||||
|
use acme_lib::persist::FilePersist;
|
||||||
|
use rust_util::XResult;
|
||||||
|
use crate::util::parse_dns_record;
|
||||||
|
use crate::network::{get_resolver, resolve_first_ipv4};
|
||||||
|
use crate::config::{AcmeChallenge, AcmeMode};
|
||||||
|
use crate::dns::{DnsClient, DnsClientFactory, DnsRecord};
|
||||||
|
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
|
||||||
|
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref TOKEN_MAP: RwLock<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct AcmeRequest<'a> {
|
||||||
|
pub challenge: AcmeChallenge,
|
||||||
|
// issue, single acme request can only process one supplier
|
||||||
|
pub credential_supplier: Option<String>,
|
||||||
|
pub allow_interact: bool,
|
||||||
|
pub contract_email: &'a str,
|
||||||
|
pub primary_name: &'a str,
|
||||||
|
pub alt_names: &'a [&'a str],
|
||||||
|
pub algo: X509PublicKeyAlgo,
|
||||||
|
pub mode: AcmeMode,
|
||||||
|
pub directory_url: Option<String>,
|
||||||
|
pub account_dir: &'a str,
|
||||||
|
pub timeout: u64,
|
||||||
|
pub local_public_ip: Option<&'a str>,
|
||||||
|
pub key_file: Option<String>,
|
||||||
|
pub cert_file: Option<String>,
|
||||||
|
pub outputs_file: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: &mut Vec<String>) -> XResult<()> {
|
||||||
|
if let Some(local_public_ip) = acme_request.local_public_ip {
|
||||||
|
let mut all_domains = vec![acme_request.primary_name.to_string()];
|
||||||
|
for alt_name in acme_request.alt_names {
|
||||||
|
all_domains.push(alt_name.to_string());
|
||||||
|
}
|
||||||
|
information!("Checking domain dns records, domains: {:?}", all_domains);
|
||||||
|
let resolver = opt_result!(get_resolver(), "Get resolver failed: {}");
|
||||||
|
|
||||||
|
if acme_request.challenge == AcmeChallenge::Http {
|
||||||
|
for domain in &all_domains {
|
||||||
|
debugging!("Checking domain: {}", domain);
|
||||||
|
let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}");
|
||||||
|
match ipv4 {
|
||||||
|
None => return simple_error!("Resolve domain ip failed: {}", domain),
|
||||||
|
Some(ipv4) => if local_public_ip != ipv4 {
|
||||||
|
return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
information!("Acme mode: {:?}", acme_request.mode);
|
||||||
|
let url = if let Some(directory_url) = &acme_request.directory_url {
|
||||||
|
DirectoryUrl::Other(directory_url)
|
||||||
|
} else {
|
||||||
|
acme_request.mode.directory_url()
|
||||||
|
};
|
||||||
|
debugging!("Directory URL: {:?}", url);
|
||||||
|
let persist = FilePersist::new(acme_request.account_dir);
|
||||||
|
let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}");
|
||||||
|
let acc = opt_result!(dir.account(acme_request.contract_email), "Directory set account failed: {}");
|
||||||
|
let mut ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}");
|
||||||
|
let mut dns_client: Option<Box<dyn DnsClient>> = match &acme_request.credential_supplier {
|
||||||
|
Some(credential_supplier) => Some(
|
||||||
|
opt_result!(DnsClientFactory::build(credential_supplier), "Build dns client failed: {}")),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut order_csr_index = 0;
|
||||||
|
let ord_csr = loop {
|
||||||
|
if let Some(ord_csr) = ord_new.confirm_validations() {
|
||||||
|
debugging!("Valid acme certificate http challenge success");
|
||||||
|
break ord_csr;
|
||||||
|
}
|
||||||
|
|
||||||
|
information!("Loop for acme challenge auth, #{}", order_csr_index);
|
||||||
|
order_csr_index += 1;
|
||||||
|
|
||||||
|
debugging!("Start acme certificate http challenge");
|
||||||
|
let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}");
|
||||||
|
for auth in &auths {
|
||||||
|
match acme_request.challenge {
|
||||||
|
AcmeChallenge::Http => {
|
||||||
|
let chall = auth.http_challenge();
|
||||||
|
let token = chall.http_token();
|
||||||
|
let proof = chall.http_proof();
|
||||||
|
|
||||||
|
{
|
||||||
|
information!("Add acme http challenge: {} -> {}",token, proof);
|
||||||
|
TOKEN_MAP.write().unwrap().insert(token.to_string(), proof);
|
||||||
|
}
|
||||||
|
debugging!("Valid acme certificate http challenge");
|
||||||
|
opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}");
|
||||||
|
}
|
||||||
|
AcmeChallenge::Dns => {
|
||||||
|
let chall = auth.dns_challenge();
|
||||||
|
let record = format!("_acme-challenge.{}.", auth.domain_name());
|
||||||
|
let proof = chall.dns_proof();
|
||||||
|
information!("Add acme dns challenge: {} -> {}", record, proof);
|
||||||
|
|
||||||
|
let rr_and_domain = opt_result!(parse_dns_record(&record), "Parse record to rr&domain failed: {}");
|
||||||
|
|
||||||
|
if !dns_cleaned_domains.contains(&rr_and_domain.1) {
|
||||||
|
information!("Clearing domain: {}", &rr_and_domain.1);
|
||||||
|
dns_cleaned_domains.push(rr_and_domain.1.clone());
|
||||||
|
if let Some(client) = dns_client.as_mut() {
|
||||||
|
match client.list_dns_records(&rr_and_domain.1) {
|
||||||
|
Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e),
|
||||||
|
Ok(records) => {
|
||||||
|
for r in &records {
|
||||||
|
let rr = &r.rr;
|
||||||
|
if rr == "_acme-challenge" || rr.starts_with("_acme-challenge.") {
|
||||||
|
match client.delete_dns_record(&r.id) {
|
||||||
|
Err(e) => warning!("Delete dns: {}.{}, failed: {}", rr, r.domain, e),
|
||||||
|
Ok(_) => success!("Delete dns: {}.{}", rr, r.domain),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match &mut dns_client {
|
||||||
|
Some(client) => {
|
||||||
|
let dns_record = DnsRecord {
|
||||||
|
id: String::new(),
|
||||||
|
domain: rr_and_domain.1,
|
||||||
|
rr: rr_and_domain.0,
|
||||||
|
r#type: "TXT".into(),
|
||||||
|
ttl: -1,
|
||||||
|
value: proof,
|
||||||
|
};
|
||||||
|
opt_result!(client.add_dns_record(&dns_record), "Add DNS TXT record failed: {}");
|
||||||
|
success!("Add dns txt record successes: {}.{} -> {}", dns_record.rr, dns_record.domain, dns_record.value);
|
||||||
|
}
|
||||||
|
None => if acme_request.allow_interact {
|
||||||
|
let mut line = String::new();
|
||||||
|
information!("You need to config dns manually, press enter to continue...");
|
||||||
|
let _ = std::io::stdin().read_line(&mut line).unwrap();
|
||||||
|
information!("Continued")
|
||||||
|
} else {
|
||||||
|
return simple_error!("Interact is not allowed, --allow-interact to allow interact");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugging!("Valid acme certificate dns challenge");
|
||||||
|
opt_result!(chall.validate(acme_request.timeout), "Validate dns challenge failed: {}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugging!("Refresh acme certificate order");
|
||||||
|
opt_result!(ord_new.refresh(), "Refresh order failed: {}");
|
||||||
|
};
|
||||||
|
|
||||||
|
information!("Generate private key, key type: {:?}", acme_request.algo);
|
||||||
|
let pkey_pri = match acme_request.algo {
|
||||||
|
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp256r1) => create_p256_key(),
|
||||||
|
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp384r1) => create_p384_key(),
|
||||||
|
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp521r1) => return simple_error!("Algo ec521 is not supported"),
|
||||||
|
X509PublicKeyAlgo::Rsa(bits) => create_rsa_key(bits),
|
||||||
|
};
|
||||||
|
|
||||||
|
debugging!("Invoking csr finalize pkey");
|
||||||
|
let ord_cert = opt_result!( ord_csr.finalize_pkey(pkey_pri, acme_request.timeout), "Submit CSR failed: {}");
|
||||||
|
debugging!("Downloading and save cert");
|
||||||
|
let cert = opt_result!( ord_cert.download_and_save_cert(), "Download and save certificate failed: {}");
|
||||||
|
|
||||||
|
if let (Some(cert_file), Some(key_file)) = (&acme_request.cert_file, &acme_request.key_file) {
|
||||||
|
debugging!("Certificate key: {}", cert.private_key());
|
||||||
|
debugging!("Certificate pem: {}", cert.certificate());
|
||||||
|
information!("Write file: {}", cert_file);
|
||||||
|
if let Err(e) = fs::write(cert_file, cert.certificate()) {
|
||||||
|
failure!("Write file: {}, failed: {}", cert_file, e);
|
||||||
|
}
|
||||||
|
information!("Write file: {}", key_file);
|
||||||
|
if let Err(e) = fs::write(key_file, cert.private_key()) {
|
||||||
|
failure!("Write file: {}, failed: {}", key_file, e);
|
||||||
|
}
|
||||||
|
success!("Write files success: {} and {}", cert_file, key_file);
|
||||||
|
} else if let Some(outputs_file) = &acme_request.outputs_file {
|
||||||
|
let mut outputs = String::new();
|
||||||
|
outputs.push_str("private key:\n");
|
||||||
|
outputs.push_str(cert.private_key());
|
||||||
|
outputs.push_str("\n\ncertificates:\n");
|
||||||
|
outputs.push_str(cert.certificate());
|
||||||
|
if let Err(e) = fs::write(outputs_file, outputs) {
|
||||||
|
failure!("Write file: {}, failed: {}", outputs_file, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
information!("Certificate key: {}", cert.private_key());
|
||||||
|
information!("Certificate pem: {}", cert.certificate());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,10 +1,54 @@
|
|||||||
|
#![allow(deprecated)]
|
||||||
|
use rust_util::XResult;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use aliyun_openapi_core_rust_sdk::RPClient;
|
use aliyun_openapi_core_rust_sdk::RPClient;
|
||||||
use rust_util::XResult;
|
use crate::dns::DnsClient;
|
||||||
|
|
||||||
static ALI_DNS_ENDPOINT: &str = "https://alidns.aliyuncs.com";
|
static ALI_DNS_ENDPOINT: &str = "https://alidns.aliyuncs.com";
|
||||||
static ALI_DNS_API_VERSION: &str = "2015-01-09";
|
static ALI_DNS_API_VERSION: &str = "2015-01-09";
|
||||||
|
|
||||||
|
pub struct AlibabaCloudDnsClient {
|
||||||
|
client: RPClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlibabaCloudDnsClient {
|
||||||
|
pub fn build(supplier: &str) -> XResult<AlibabaCloudDnsClient> {
|
||||||
|
let access_credential = simple_parse_aliyun_supplier(supplier)?;
|
||||||
|
Ok(AlibabaCloudDnsClient {
|
||||||
|
client: build_dns_client(&access_credential)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DnsClient for AlibabaCloudDnsClient {
|
||||||
|
fn list_dns_records(&mut self, domain: &str) -> XResult<Vec<crate::dns::DnsRecord>> {
|
||||||
|
let list_dns_response = opt_result!(list_dns(&self.client, domain)?, "List dns records failed: {:?}");
|
||||||
|
let mut dns_records = vec![];
|
||||||
|
list_dns_response.domain_records.record.into_iter().for_each(|record| {
|
||||||
|
dns_records.push(crate::dns::DnsRecord {
|
||||||
|
id: record.record_id,
|
||||||
|
domain: record.domain_name,
|
||||||
|
rr: record.rr,
|
||||||
|
r#type: record.r#type,
|
||||||
|
ttl: record.ttl,
|
||||||
|
value: record.value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Ok(dns_records)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_dns_record(&mut self, record_id: &str) -> XResult<()> {
|
||||||
|
opt_result!(delete_dns_record(&self.client, record_id)?, "Delete dns record failed: {:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_dns_record(&mut self, dns_record: &crate::dns::DnsRecord) -> XResult<()> {
|
||||||
|
let _ = opt_result!(add_dns_record(&self.client, &dns_record.domain, &dns_record.rr, &dns_record.r#type, &dns_record.value),
|
||||||
|
"Add dns record failed: {}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AccessCredential {
|
pub struct AccessCredential {
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
@@ -111,9 +155,9 @@ pub fn delete_dns_record(client: &RPClient, record_id: &str) -> XResult<Result<C
|
|||||||
parse_result("DeleteDomainRecord", &delete_domain_record_response)
|
parse_result("DeleteDomainRecord", &delete_domain_record_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_txt_dns_record(client: &RPClient, domain: &str, rr: &str, value: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
|
// pub fn add_txt_dns_record(client: &RPClient, domain: &str, rr: &str, value: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
|
||||||
add_dns_record(client, domain, rr, "TXT", value)
|
// add_dns_record(client, domain, rr, "TXT", value)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// domain -> "example.com"
|
// domain -> "example.com"
|
||||||
// rr -> "@", "_acme-challenge"
|
// rr -> "@", "_acme-challenge"
|
||||||
@@ -142,11 +186,11 @@ pub fn build_dns_client(access_credential: &AccessCredential) -> RPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_result<'a, S, E>(fn_name: &str, response: &'a str) -> XResult<Result<S, E>> where S: Deserialize<'a>, E: Deserialize<'a> {
|
fn parse_result<'a, S, E>(fn_name: &str, response: &'a str) -> XResult<Result<S, E>> where S: Deserialize<'a>, E: Deserialize<'a> {
|
||||||
let describe_domain_records_result: serde_json::Result<S> = serde_json::from_str(&response);
|
let describe_domain_records_result: serde_json::Result<S> = serde_json::from_str(response);
|
||||||
match describe_domain_records_result {
|
match describe_domain_records_result {
|
||||||
Ok(r) => Ok(Ok(r)),
|
Ok(r) => Ok(Ok(r)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let describe_domain_records_error_result: serde_json::Result<E> = serde_json::from_str(&response);
|
let describe_domain_records_error_result: serde_json::Result<E> = serde_json::from_str(response);
|
||||||
match describe_domain_records_error_result {
|
match describe_domain_records_error_result {
|
||||||
Ok(r) => Ok(Err(r)),
|
Ok(r) => Ok(Err(r)),
|
||||||
Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response),
|
Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response),
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use rust_util::XResult;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use acme_lib::DirectoryUrl;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use crate::x509;
|
|
||||||
use crate::x509::{X509PublicKeyAlgo, X509Certificate};
|
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use rust_util::XResult;
|
||||||
|
use acme_lib::DirectoryUrl;
|
||||||
|
use crate::x509::{X509PublicKeyAlgo, X509Certificate, parse_x509};
|
||||||
|
|
||||||
pub const CERT_NAME: &str = "cert.pem";
|
pub const CERT_NAME: &str = "cert.pem";
|
||||||
pub const KEY_NAME: &str = "key.pem";
|
pub const KEY_NAME: &str = "key.pem";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AcmeChallenge {
|
pub enum AcmeChallenge {
|
||||||
|
#[default]
|
||||||
Http,
|
Http,
|
||||||
Dns,
|
Dns,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AcmeChallenge {
|
impl AcmeChallenge {
|
||||||
fn default() -> Self {
|
pub fn from_str(t: Option<&str>) -> Self {
|
||||||
|
let t = t.map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string());
|
||||||
|
if t == "dns" {
|
||||||
|
AcmeChallenge::Dns
|
||||||
|
} else {
|
||||||
AcmeChallenge::Http
|
AcmeChallenge::Http
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -74,6 +79,7 @@ pub struct CertConfig {
|
|||||||
pub cert_items: Vec<CertConfigItem>,
|
pub cert_items: Vec<CertConfigItem>,
|
||||||
pub trigger_after_update: Option<Vec<String>>,
|
pub trigger_after_update: Option<Vec<String>>,
|
||||||
pub notify_token: Option<String>,
|
pub notify_token: Option<String>,
|
||||||
|
pub directory_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CertConfig {
|
impl CertConfig {
|
||||||
@@ -123,6 +129,7 @@ impl CertConfig {
|
|||||||
cert_items: filtered_cert_items,
|
cert_items: filtered_cert_items,
|
||||||
trigger_after_update: self.trigger_after_update,
|
trigger_after_update: self.trigger_after_update,
|
||||||
notify_token: self.notify_token,
|
notify_token: self.notify_token,
|
||||||
|
directory_url: self.directory_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +142,7 @@ impl CertConfig {
|
|||||||
|
|
||||||
impl CertConfigItem {
|
impl CertConfigItem {
|
||||||
pub fn get_acme_challenge(&self) -> AcmeChallenge {
|
pub fn get_acme_challenge(&self) -> AcmeChallenge {
|
||||||
let t = self.r#type.as_ref().map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string());
|
AcmeChallenge::from_str(self.r#type.as_deref())
|
||||||
if t == "dns" {
|
|
||||||
AcmeChallenge::Dns
|
|
||||||
} else {
|
|
||||||
AcmeChallenge::Http
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_dns_names(&mut self) -> XResult<Option<X509Certificate>> {
|
pub fn fill_dns_names(&mut self) -> XResult<Option<X509Certificate>> {
|
||||||
@@ -151,7 +153,7 @@ impl CertConfigItem {
|
|||||||
let cert_path_buff = path_buff.join(CERT_NAME);
|
let cert_path_buff = path_buff.join(CERT_NAME);
|
||||||
if self.common_name.is_none() && self.dns_names.is_none() {
|
if self.common_name.is_none() && self.dns_names.is_none() {
|
||||||
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
|
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
|
||||||
let x509_certificate = opt_result!(x509::parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
|
let x509_certificate = opt_result!(parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
|
||||||
self.common_name = Some(x509_certificate.common_name.clone());
|
self.common_name = Some(x509_certificate.common_name.clone());
|
||||||
self.dns_names = Some(x509_certificate.alt_names.clone());
|
self.dns_names = Some(x509_certificate.alt_names.clone());
|
||||||
if let Some(pos) = x509_certificate.alt_names.iter().position(|n| n == &x509_certificate.common_name) {
|
if let Some(pos) = x509_certificate.alt_names.iter().position(|n| n == &x509_certificate.common_name) {
|
||||||
@@ -171,7 +173,7 @@ impl CertConfigItem {
|
|||||||
if self.public_key_algo.is_none() {
|
if self.public_key_algo.is_none() {
|
||||||
self.public_key_algo = match &self.algo {
|
self.public_key_algo = match &self.algo {
|
||||||
None => Some(X509PublicKeyAlgo::Rsa(2048)),
|
None => Some(X509PublicKeyAlgo::Rsa(2048)),
|
||||||
Some(algo) => match X509PublicKeyAlgo::from_str(&algo) {
|
Some(algo) => match X509PublicKeyAlgo::from_str(algo) {
|
||||||
Ok(algo) => Some(algo),
|
Ok(algo) => Some(algo),
|
||||||
Err(_) => return simple_error!("Unknown algo: {}", algo),
|
Err(_) => return simple_error!("Unknown algo: {}", algo),
|
||||||
},
|
},
|
||||||
@@ -179,7 +181,7 @@ impl CertConfigItem {
|
|||||||
}
|
}
|
||||||
if cert_path_buff.exists() {
|
if cert_path_buff.exists() {
|
||||||
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
|
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
|
||||||
let x509_certificate = opt_result!(x509::parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
|
let x509_certificate = opt_result!(parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
|
||||||
|
|
||||||
let mut self_dns_names = vec![];
|
let mut self_dns_names = vec![];
|
||||||
let mut cert_dns_names = vec![];
|
let mut cert_dns_names = vec![];
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
use std::io::{Error, ErrorKind};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use std::time::SystemTime;
|
|
||||||
use rust_util::XResult;
|
|
||||||
use hmac::{Hmac, Mac};
|
|
||||||
use sha2::Sha256;
|
|
||||||
use crate::CertConfig;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct InnerTextMessageText {
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct InnerTextMessage {
|
|
||||||
pub msgtype: String,
|
|
||||||
pub text: InnerTextMessageText,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_dingtalk_message(cert_config: &CertConfig, message: &str) -> XResult<()> {
|
|
||||||
let dintalk_notify_token = get_dingtalk_notify_token(cert_config);
|
|
||||||
if let Some((access_token, sec_token)) = &dintalk_notify_token {
|
|
||||||
inner_send_dingtalk_message(access_token, sec_token, message)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inner_send_dingtalk_message(access_token: &str, sec_token: &str, message: &str) -> XResult<()> {
|
|
||||||
let dingtalk_message_json = serde_json::to_string(&InnerTextMessage {
|
|
||||||
msgtype: "text".into(),
|
|
||||||
text: InnerTextMessageText {
|
|
||||||
content: message.into(),
|
|
||||||
},
|
|
||||||
})?;
|
|
||||||
let client = reqwest::blocking::Client::new();
|
|
||||||
let mut webhook_url = "https://oapi.dingtalk.com/robot/send".to_string();
|
|
||||||
webhook_url.push_str("?access_token=");
|
|
||||||
webhook_url.push_str(&urlencoding::encode(access_token));
|
|
||||||
if !sec_token.is_empty() {
|
|
||||||
let timestamp = &format!("{}", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis());
|
|
||||||
let timestamp_and_secret = &format!("{}\n{}", timestamp, sec_token);
|
|
||||||
let hmac_sha256 = base64::encode(&calc_hmac_sha256(sec_token.as_bytes(), timestamp_and_secret.as_bytes())?[..]);
|
|
||||||
webhook_url.push_str("×tamp=");
|
|
||||||
webhook_url.push_str(timestamp);
|
|
||||||
webhook_url.push_str("&sign=");
|
|
||||||
webhook_url.push_str(&urlencoding::encode(&hmac_sha256));
|
|
||||||
}
|
|
||||||
let response = client.post(webhook_url)
|
|
||||||
.header("Content-Type", "application/json; charset=utf-8")
|
|
||||||
.body(dingtalk_message_json.as_bytes().to_vec())
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
information!("Send dingtalk message: {:?}", response);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// format: dingtalk:access_token?sec_token
|
|
||||||
fn get_dingtalk_notify_token(cert_config: &CertConfig) -> Option<(String, String)> {
|
|
||||||
let token = cert_config.notify_token.as_ref()?;
|
|
||||||
if token.starts_with("dingtalk:") {
|
|
||||||
let token_and_or_sec = &token["dingtalk:".len()..];
|
|
||||||
let mut token_and_or_sec_vec = token_and_or_sec.split('?');
|
|
||||||
let access_token = match token_and_or_sec_vec.next() {
|
|
||||||
Some(t) => t,
|
|
||||||
None => token_and_or_sec,
|
|
||||||
};
|
|
||||||
let sec_token = match token_and_or_sec_vec.next() {
|
|
||||||
Some(t) => t,
|
|
||||||
None => "",
|
|
||||||
};
|
|
||||||
Some((access_token.into(), sec_token.into()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// calc hma_sha256 digest
|
|
||||||
fn calc_hmac_sha256(key: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
|
||||||
let mut mac = match Hmac::<Sha256>::new_varkey(key) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(Box::new(Error::new(ErrorKind::Other, format!("Hmac error: {}", e))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mac.input(message);
|
|
||||||
Ok(mac.result().code().to_vec())
|
|
||||||
}
|
|
||||||
30
src/dns.rs
Normal file
30
src/dns.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use rust_util::XResult;
|
||||||
|
use crate::ali_dns::AlibabaCloudDnsClient;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DnsRecord {
|
||||||
|
pub id: String,
|
||||||
|
pub domain: String,
|
||||||
|
pub rr: String,
|
||||||
|
pub r#type: String,
|
||||||
|
pub ttl: i32,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DnsClient {
|
||||||
|
fn list_dns_records(&mut self, domain: &str) -> XResult<Vec<DnsRecord>>;
|
||||||
|
|
||||||
|
fn delete_dns_record(&mut self, record_id: &str) -> XResult<()>;
|
||||||
|
|
||||||
|
fn add_dns_record(&mut self, dns_record: &DnsRecord) -> XResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DnsClientFactory {}
|
||||||
|
|
||||||
|
impl DnsClientFactory {
|
||||||
|
pub fn build(supplier: &str) -> XResult<Box<dyn DnsClient>> {
|
||||||
|
Ok(Box::new(AlibabaCloudDnsClient::build(supplier)?))
|
||||||
|
//simple_error!("Build dns client failed: {}", supplier)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/kms.rs
Normal file
30
src/kms.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use reqwest::blocking::{Body, Client, Request};
|
||||||
|
use reqwest::{Method, Url};
|
||||||
|
use rust_util::XResult;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DecryptResponse {
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_kms_decrypt(ciphertext: &str) -> XResult<String> {
|
||||||
|
if !ciphertext.starts_with("LKMS:") {
|
||||||
|
return Ok(ciphertext.to_string());
|
||||||
|
}
|
||||||
|
debugging!("Try decrypt: {}", ciphertext);
|
||||||
|
let body = json!({ "encrypted_value": ciphertext });
|
||||||
|
let body = serde_json::to_string(&body)?;
|
||||||
|
let client = Client::new();
|
||||||
|
let uri = format!("http://{}/{}", "127.0.0.1:5567", "decrypt");
|
||||||
|
let mut request = Request::new(Method::POST, Url::parse(&uri)?);
|
||||||
|
let _ = request.body_mut().insert(Body::from(body));
|
||||||
|
let response = client.execute(request)?;
|
||||||
|
debugging!("KMS response text: {:?}", &response);
|
||||||
|
let response_text = response.text()?;
|
||||||
|
debugging!("KMS response text: {}", &response_text);
|
||||||
|
let decrypt_response: DecryptResponse = serde_json::from_str(&response_text)?;
|
||||||
|
debugging!("Decrypt value: {}", &decrypt_response.value);
|
||||||
|
Ok(decrypt_response.value)
|
||||||
|
}
|
||||||
309
src/main.rs
309
src/main.rs
@@ -3,71 +3,40 @@ extern crate lazy_static;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rust_util;
|
extern crate rust_util;
|
||||||
|
|
||||||
|
mod acme;
|
||||||
mod util;
|
mod util;
|
||||||
mod config;
|
mod config;
|
||||||
mod x509;
|
mod x509;
|
||||||
mod network;
|
mod network;
|
||||||
mod statics;
|
mod statistics;
|
||||||
mod dingtalk;
|
mod notification;
|
||||||
|
mod kms;
|
||||||
|
mod dns;
|
||||||
mod ali_dns;
|
mod ali_dns;
|
||||||
// mod simple_thread_pool;
|
// mod simple_thread_pool;
|
||||||
|
|
||||||
use std::env;
|
use crate::acme::{request_acme_certificate, AcmeRequest};
|
||||||
use rust_util::XResult;
|
use crate::config::{AcmeChallenge, AcmeMode, CertConfig, CERT_NAME, KEY_NAME};
|
||||||
use acme_lib::Directory;
|
use crate::notification::send_notify_message;
|
||||||
use acme_lib::{create_p384_key, create_p256_key, create_rsa_key};
|
use crate::network::get_local_public_ip;
|
||||||
use acme_lib::persist::FilePersist;
|
use crate::statistics::{AcmeStatistics, AcmeStatus};
|
||||||
|
use crate::x509::X509PublicKeyAlgo;
|
||||||
|
use async_std::{channel, channel::Sender, task};
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg};
|
||||||
use std::fs;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use tide::Request;
|
|
||||||
use std::process::{Command, exit};
|
|
||||||
use std::time::{Duration, SystemTime};
|
|
||||||
use async_std::task;
|
|
||||||
use async_std::channel;
|
|
||||||
use async_std::channel::Sender;
|
|
||||||
use config::AcmeMode;
|
|
||||||
use crate::config::{AcmeChallenge, CertConfig, CERT_NAME, KEY_NAME};
|
|
||||||
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use aliyun_openapi_core_rust_sdk::RPClient;
|
|
||||||
use rust_util::util_cmd::run_command_and_wait;
|
use rust_util::util_cmd::run_command_and_wait;
|
||||||
use crate::ali_dns::{add_txt_dns_record, build_dns_client, delete_dns_record, list_dns, simple_parse_aliyun_supplier};
|
use std::env;
|
||||||
use crate::dingtalk::send_dingtalk_message;
|
use std::fs;
|
||||||
use crate::network::{get_local_public_ip, get_resolver, resolve_first_ipv4};
|
use std::path::PathBuf;
|
||||||
use crate::statics::{AcmeStatics, AcmeStatus};
|
use std::process::{exit, Command};
|
||||||
use crate::util::parse_dns_record;
|
use std::str::FromStr;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use tide::Request;
|
||||||
|
|
||||||
const NAME: &str = env!("CARGO_PKG_NAME");
|
const NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||||
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref TOKEN_MAP: RwLock<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct AcmeRequest<'a> {
|
|
||||||
challenge: AcmeChallenge,
|
|
||||||
// issue, single acme request can only process one supplier
|
|
||||||
credential_supplier: Option<&'a str>,
|
|
||||||
allow_interact: bool,
|
|
||||||
contract_email: &'a str,
|
|
||||||
primary_name: &'a str,
|
|
||||||
alt_names: &'a [&'a str],
|
|
||||||
algo: X509PublicKeyAlgo,
|
|
||||||
mode: AcmeMode,
|
|
||||||
account_dir: &'a str,
|
|
||||||
timeout: u64,
|
|
||||||
local_public_ip: Option<&'a str>,
|
|
||||||
key_file: Option<String>,
|
|
||||||
cert_file: Option<String>,
|
|
||||||
outputs_file: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
async fn main() -> tide::Result<()> {
|
async fn main() -> tide::Result<()> {
|
||||||
let matches = App::new(NAME)
|
let matches = App::new(NAME)
|
||||||
@@ -76,13 +45,13 @@ async fn main() -> tide::Result<()> {
|
|||||||
.author(AUTHORS)
|
.author(AUTHORS)
|
||||||
.arg(Arg::with_name("version").short("V").long("version").help("Print version"))
|
.arg(Arg::with_name("version").short("V").long("version").help("Print version"))
|
||||||
.arg(Arg::with_name("verbose").short("v").long("verbose").help("Verbose"))
|
.arg(Arg::with_name("verbose").short("v").long("verbose").help("Verbose"))
|
||||||
.arg(Arg::with_name("type").short("t").long("type").default_value("http").takes_value(true).help("Type http or dns"))
|
|
||||||
.arg(Arg::with_name("port").short("p").long("port").default_value("80").takes_value(true).help("Http port"))
|
.arg(Arg::with_name("port").short("p").long("port").default_value("80").takes_value(true).help("Http port"))
|
||||||
.arg(Arg::with_name("domain").short("d").long("domain").multiple(true).takes_value(true).help("Domains"))
|
.arg(Arg::with_name("domain").short("d").long("domain").multiple(true).takes_value(true).help("Domains"))
|
||||||
.arg(Arg::with_name("email").long("email").takes_value(true).help("Contract email"))
|
.arg(Arg::with_name("email").long("email").takes_value(true).help("Contract email"))
|
||||||
.arg(Arg::with_name("algo").short("a").long("algo").takes_value(true).default_value("ec384").help("Pki algo"))
|
.arg(Arg::with_name("algo").short("a").long("algo").takes_value(true).default_value("ec384").help("Pki algo"))
|
||||||
.arg(Arg::with_name("timeout").long("timeout").takes_value(true).default_value("5000").help("Timeout (ms)"))
|
.arg(Arg::with_name("timeout").long("timeout").takes_value(true).default_value("5000").help("Timeout (ms)"))
|
||||||
.arg(Arg::with_name("mode").short("m").long("mode").takes_value(true).default_value("prod").help("Mode"))
|
.arg(Arg::with_name("mode").short("m").long("mode").takes_value(true).default_value("prod").help("Mode"))
|
||||||
|
.arg(Arg::with_name("directory-url").long("directory-url").takes_value(true).help("ACME directory URL"))
|
||||||
.arg(Arg::with_name("dir").long("dir").takes_value(true).default_value("acme_dir").help("Account key dir"))
|
.arg(Arg::with_name("dir").long("dir").takes_value(true).default_value("acme_dir").help("Account key dir"))
|
||||||
.arg(Arg::with_name("cert-dir").long("cert-dir").takes_value(true).help("Certificate dir"))
|
.arg(Arg::with_name("cert-dir").long("cert-dir").takes_value(true).help("Certificate dir"))
|
||||||
.arg(Arg::with_name("config").short("c").long("config").takes_value(true).help("Cert config"))
|
.arg(Arg::with_name("config").short("c").long("config").takes_value(true).help("Cert config"))
|
||||||
@@ -91,7 +60,10 @@ async fn main() -> tide::Result<()> {
|
|||||||
.arg(Arg::with_name("hide-logo").long("hide-logo").help("Hide logo"))
|
.arg(Arg::with_name("hide-logo").long("hide-logo").help("Hide logo"))
|
||||||
.arg(Arg::with_name("skip-verify-ip").short("k").long("skip-verify-ip").help("Skip verify public ip"))
|
.arg(Arg::with_name("skip-verify-ip").short("k").long("skip-verify-ip").help("Skip verify public ip"))
|
||||||
.arg(Arg::with_name("skip-verify-certificate").short("K").long("skip-verify-certificate").help("Skip verify certificate"))
|
.arg(Arg::with_name("skip-verify-certificate").short("K").long("skip-verify-certificate").help("Skip verify certificate"))
|
||||||
|
.arg(Arg::with_name("skip-listen").long("skip-listen").help("Skip http challenge listen"))
|
||||||
.arg(Arg::with_name("allow-interact").long("allow-interact").help("Allow interact"))
|
.arg(Arg::with_name("allow-interact").long("allow-interact").help("Allow interact"))
|
||||||
|
.arg(Arg::with_name("challenge-type").short("t").long("challenge-type").takes_value(true).default_value("http").help("Challenge type, http or dns"))
|
||||||
|
.arg(Arg::with_name("dns-supplier").short("s").long("dns-supplier").takes_value(true).help("DNS supplier, e.g. account://***:****@**?id=*"))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
if matches.is_present("verbose") {
|
if matches.is_present("verbose") {
|
||||||
@@ -114,7 +86,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
let skip_verify_certificate = matches.is_present("skip-verify-certificate");
|
let skip_verify_certificate = matches.is_present("skip-verify-certificate");
|
||||||
Some(get_local_public_ip(skip_verify_certificate).unwrap_or_else(|e| {
|
Some(get_local_public_ip(skip_verify_certificate).unwrap_or_else(|e| {
|
||||||
failure!("Get local public ip failed: {}", e);
|
failure!("Get local public ip failed: {}, you can turn off verify IP by -k or --skip-verify-ip", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
@@ -157,13 +129,6 @@ async fn main() -> tide::Result<()> {
|
|||||||
email.to_string()
|
email.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
match matches.value_of("type") {
|
|
||||||
Some("http") => {}
|
|
||||||
_ => {
|
|
||||||
failure!("Type is not assigned or must be http.");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let port: u16 = match matches.value_of("port") {
|
let port: u16 = match matches.value_of("port") {
|
||||||
Some(p) => p.parse().unwrap_or_else(|e| {
|
Some(p) => p.parse().unwrap_or_else(|e| {
|
||||||
failure!("Parse port: {}, failed: {}", p, e);
|
failure!("Parse port: {}, failed: {}", p, e);
|
||||||
@@ -211,11 +176,13 @@ async fn main() -> tide::Result<()> {
|
|||||||
failure!("Load cert config: {}, failed: {}", f, e);
|
failure!("Load cert config: {}, failed: {}", f, e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}));
|
}));
|
||||||
let port = cert_config.as_ref().map(|c| c.port).flatten().unwrap_or(port);
|
|
||||||
|
|
||||||
let check = matches.is_present("check");
|
let skip_listen = matches.is_present("skip-listen");
|
||||||
|
let port = iff!(skip_listen, 0, cert_config.as_ref().and_then(|c| c.port).unwrap_or(port));
|
||||||
|
|
||||||
if !check {
|
let check_config = matches.is_present("check");
|
||||||
|
|
||||||
|
if !check_config && port > 0 {
|
||||||
let (s, r) = channel::bounded(1);
|
let (s, r) = channel::bounded(1);
|
||||||
startup_http_server(s, port);
|
startup_http_server(s, port);
|
||||||
r.recv().await.ok();
|
r.recv().await.ok();
|
||||||
@@ -225,7 +192,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
let mut dns_cleaned_domains: Vec<String> = vec![];
|
let mut dns_cleaned_domains: Vec<String> = vec![];
|
||||||
match cert_config {
|
match cert_config {
|
||||||
None => { // cert config is not assigned
|
None => { // cert config is not assigned
|
||||||
if check {
|
if check_config {
|
||||||
failure!("Bad argument `--check`");
|
failure!("Bad argument `--check`");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -249,21 +216,22 @@ async fn main() -> tide::Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let acme_request = AcmeRequest {
|
let acme_request = AcmeRequest {
|
||||||
challenge: AcmeChallenge::Http,
|
challenge: AcmeChallenge::from_str(matches.value_of("challenge-type")),
|
||||||
credential_supplier: None,
|
credential_supplier: matches.value_of("dns-supplier").map(ToString::to_string),
|
||||||
allow_interact: false,
|
allow_interact: matches.is_present("allow-interact"),
|
||||||
contract_email: &email,
|
contract_email: &email,
|
||||||
primary_name,
|
primary_name,
|
||||||
alt_names: &alt_names,
|
alt_names: &alt_names,
|
||||||
algo,
|
algo,
|
||||||
mode,
|
mode,
|
||||||
|
directory_url: matches.value_of("directory-url").map(|u| u.to_string()),
|
||||||
account_dir,
|
account_dir,
|
||||||
timeout,
|
timeout,
|
||||||
local_public_ip: local_public_ip.as_deref(),
|
local_public_ip: local_public_ip.as_deref(),
|
||||||
cert_file,
|
cert_file,
|
||||||
key_file,
|
key_file,
|
||||||
outputs_file: matches.value_of("outputs").map(|s| s.into()),
|
outputs_file: matches.value_of("outputs").map(|s| s.into()),
|
||||||
..Default::default()
|
//..Default::default()
|
||||||
};
|
};
|
||||||
if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) {
|
if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) {
|
||||||
failure!("Request certificate by acme failed: {}", e);
|
failure!("Request certificate by acme failed: {}", e);
|
||||||
@@ -271,13 +239,14 @@ async fn main() -> tide::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(cert_config) => { // cert config is assigned
|
Some(cert_config) => { // cert config is assigned
|
||||||
if check {
|
if check_config {
|
||||||
check_cert_config(&cert_config);
|
check_cert_config(&cert_config);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut acme_statics = AcmeStatics::start();
|
let mut acme_statistics = AcmeStatistics::start();
|
||||||
let filtered_cert_config = cert_config.filter_cert_config_items(30);
|
let filtered_cert_config = cert_config.filter_cert_config_items(30);
|
||||||
for item in &filtered_cert_config.cert_items {
|
for item in &filtered_cert_config.cert_items {
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
if item.common_name.as_ref().map(|n| n.contains('*')).unwrap_or(false)
|
if item.common_name.as_ref().map(|n| n.contains('*')).unwrap_or(false)
|
||||||
|| item.dns_names.as_ref().map(|dns_names| dns_names.iter().any(|n| n.contains('*'))).unwrap_or(false) {
|
|| item.dns_names.as_ref().map(|dns_names| dns_names.iter().any(|n| n.contains('*'))).unwrap_or(false) {
|
||||||
if item.get_acme_challenge() != AcmeChallenge::Dns {
|
if item.get_acme_challenge() != AcmeChallenge::Dns {
|
||||||
@@ -294,13 +263,19 @@ async fn main() -> tide::Result<()> {
|
|||||||
None => None,
|
None => None,
|
||||||
Some(supplier) => {
|
Some(supplier) => {
|
||||||
let credential_supplier = filtered_cert_config.credential_suppliers.as_ref()
|
let credential_supplier = filtered_cert_config.credential_suppliers.as_ref()
|
||||||
.map(|m| m.get(supplier)).flatten();
|
.and_then(|m| m.get(supplier));
|
||||||
match credential_supplier {
|
match credential_supplier {
|
||||||
None => {
|
None => {
|
||||||
warning!("DNS challenge no credential supplier found");
|
warning!("DNS challenge no credential supplier found");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Some(credential_supplier) => Some(credential_supplier.as_str()),
|
Some(credential_supplier) => match kms::try_kms_decrypt(credential_supplier) {
|
||||||
|
Ok(credential_supplier) => Some(credential_supplier),
|
||||||
|
Err(e) => {
|
||||||
|
failure!("Decrypt DNS challenge credential supplier failed: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,6 +289,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
alt_names: &alt_names,
|
alt_names: &alt_names,
|
||||||
algo,
|
algo,
|
||||||
mode,
|
mode,
|
||||||
|
directory_url: matches.value_of("directory-url").map(|u| u.to_string()).or(filtered_cert_config.directory_url.clone()),
|
||||||
account_dir,
|
account_dir,
|
||||||
timeout,
|
timeout,
|
||||||
local_public_ip: local_public_ip.as_deref(),
|
local_public_ip: local_public_ip.as_deref(),
|
||||||
@@ -325,30 +301,30 @@ async fn main() -> tide::Result<()> {
|
|||||||
dns_names.iter().for_each(|dns_name| domains.push(dns_name.clone()));
|
dns_names.iter().for_each(|dns_name| domains.push(dns_name.clone()));
|
||||||
if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) {
|
if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) {
|
||||||
failure!("Request certificate: {}, by acme failed: {}", item.path, e);
|
failure!("Request certificate: {}, by acme failed: {}", item.path, e);
|
||||||
acme_statics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
|
acme_statistics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
|
||||||
} else {
|
} else {
|
||||||
acme_statics.add_item(domains, AcmeStatus::Success);
|
acme_statistics.add_item(domains, AcmeStatus::Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acme_statics.end();
|
acme_statistics.end();
|
||||||
|
|
||||||
let mut success_count = 0;
|
let mut success_count = 0;
|
||||||
for acme_static in &acme_statics.items {
|
for acme_statistic in &acme_statistics.items {
|
||||||
if let AcmeStatus::Success = acme_static.status {
|
if let AcmeStatus::Success = acme_statistic.status {
|
||||||
success_count += 1;
|
success_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
success!("Statics: \n{}", acme_statics);
|
success!("Statistics: \n{}", acme_statistics);
|
||||||
|
|
||||||
let mut dingtalk_message = format!("Statics: \n{}", acme_statics);
|
let mut dingtalk_message = format!("Statistics: \n{}", acme_statistics);
|
||||||
if success_count > 0 {
|
if success_count > 0 {
|
||||||
if let Some(trigger_after_update) = &filtered_cert_config.trigger_after_update {
|
if let Some(trigger_after_update) = &filtered_cert_config.trigger_after_update {
|
||||||
if trigger_after_update.len() > 0 {
|
if !trigger_after_update.is_empty() {
|
||||||
let mut cmd = Command::new(&trigger_after_update[0]);
|
let mut cmd = Command::new(&trigger_after_update[0]);
|
||||||
for i in 1..trigger_after_update.len() {
|
for arg in trigger_after_update.iter().skip(1) {
|
||||||
cmd.arg(&trigger_after_update[i]);
|
cmd.arg(arg);
|
||||||
}
|
}
|
||||||
match run_command_and_wait(&mut cmd) {
|
match run_command_and_wait(&mut cmd) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -370,7 +346,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
|
|
||||||
let mut success_domains = vec![];
|
let mut success_domains = vec![];
|
||||||
let mut failed_domains = vec![];
|
let mut failed_domains = vec![];
|
||||||
for acme_item in &acme_statics.items {
|
for acme_item in &acme_statistics.items {
|
||||||
if let AcmeStatus::Success = acme_item.status {
|
if let AcmeStatus::Success = acme_item.status {
|
||||||
success_domains.push(format!("* {}", acme_item.domains.join(", ")));
|
success_domains.push(format!("* {}", acme_item.domains.join(", ")));
|
||||||
}
|
}
|
||||||
@@ -388,12 +364,12 @@ async fn main() -> tide::Result<()> {
|
|||||||
dingtalk_message.push_str(&failed_domains.join("\n"));
|
dingtalk_message.push_str(&failed_domains.join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !acme_statics.items.is_empty() && filtered_cert_config.notify_token.is_some() {
|
if !acme_statistics.items.is_empty() && filtered_cert_config.notify_token.is_some() {
|
||||||
if let Err(err) = send_dingtalk_message(&filtered_cert_config, &dingtalk_message) {
|
if let Err(err) = send_notify_message(&filtered_cert_config, &dingtalk_message) {
|
||||||
failure!("Send notification message failed: {:?}", err);
|
failure!("Send notification message failed: {:?}", err);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
information!("No notification message need to send, or not configed notification token");
|
information!("No notification message sent, or no configured notification token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,167 +406,6 @@ fn check_cert_config(cert_config: &CertConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: &mut Vec<String>) -> XResult<()> {
|
|
||||||
if let Some(local_public_ip) = acme_request.local_public_ip {
|
|
||||||
let mut all_domains = vec![acme_request.primary_name.to_string()];
|
|
||||||
for alt_name in acme_request.alt_names {
|
|
||||||
all_domains.push(alt_name.to_string());
|
|
||||||
}
|
|
||||||
information!("Checking domain dns records, domains: {:?}", all_domains);
|
|
||||||
let resolver = opt_result!(get_resolver(), "Get resolver failed: {}");
|
|
||||||
|
|
||||||
if acme_request.challenge == AcmeChallenge::Http {
|
|
||||||
for domain in &all_domains {
|
|
||||||
debugging!("Checking domain: {}", domain);
|
|
||||||
let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}");
|
|
||||||
match ipv4 {
|
|
||||||
None => return simple_error!("Resolve domain ip failed: {}", domain),
|
|
||||||
Some(ipv4) => if local_public_ip != ipv4 {
|
|
||||||
return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
information!("Acme mode: {:?}", acme_request.mode);
|
|
||||||
let url = acme_request.mode.directory_url();
|
|
||||||
let persist = FilePersist::new(acme_request.account_dir);
|
|
||||||
let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}");
|
|
||||||
let acc = opt_result!(dir.account(acme_request.contract_email), "Directory set account failed: {}");
|
|
||||||
let mut ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}");
|
|
||||||
let ali_yun_client: Option<RPClient> = match acme_request.credential_supplier {
|
|
||||||
Some(credential_supplier) => Some(build_dns_client(
|
|
||||||
&opt_result!(simple_parse_aliyun_supplier(credential_supplier), "Parse credential supplier failed: {}"))),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut order_csr_index = 0;
|
|
||||||
let ord_csr = loop {
|
|
||||||
if let Some(ord_csr) = ord_new.confirm_validations() {
|
|
||||||
debugging!("Valid acme certificate http challenge success");
|
|
||||||
break ord_csr;
|
|
||||||
}
|
|
||||||
|
|
||||||
information!("Loop for acme challenge auth, #{}", order_csr_index);
|
|
||||||
order_csr_index += 1;
|
|
||||||
|
|
||||||
debugging!("Start acme certificate http challenge");
|
|
||||||
let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}");
|
|
||||||
for auth in &auths {
|
|
||||||
match acme_request.challenge {
|
|
||||||
AcmeChallenge::Http => {
|
|
||||||
let chall = auth.http_challenge();
|
|
||||||
let token = chall.http_token();
|
|
||||||
let proof = chall.http_proof();
|
|
||||||
|
|
||||||
{
|
|
||||||
information!("Add acme http challenge: {} -> {}",token, proof);
|
|
||||||
TOKEN_MAP.write().unwrap().insert(token.to_string(), proof);
|
|
||||||
}
|
|
||||||
debugging!("Valid acme certificate http challenge");
|
|
||||||
opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}");
|
|
||||||
}
|
|
||||||
AcmeChallenge::Dns => {
|
|
||||||
let chall = auth.dns_challenge();
|
|
||||||
let record = format!("_acme-challenge.{}.", auth.domain_name());
|
|
||||||
let proof = chall.dns_proof();
|
|
||||||
information!("Add acme dns challenge: {} -> {}", record, proof);
|
|
||||||
|
|
||||||
let rr_and_domain = opt_result!(parse_dns_record(&record), "Parse record to rr&domain failed: {}");
|
|
||||||
|
|
||||||
if !dns_cleaned_domains.contains(&rr_and_domain.1) {
|
|
||||||
information!("Clearing domain: {}", &rr_and_domain.1);
|
|
||||||
dns_cleaned_domains.push(rr_and_domain.1.clone());
|
|
||||||
ali_yun_client.as_ref().map(|client| {
|
|
||||||
match list_dns(client, &rr_and_domain.1) {
|
|
||||||
Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e),
|
|
||||||
Ok(Err(e)) => warning!("List dns for: {}, failed: {:?}", &rr_and_domain.1, e),
|
|
||||||
Ok(Ok(s)) => {
|
|
||||||
for r in &s.domain_records.record {
|
|
||||||
let rr = &r.rr;
|
|
||||||
if rr == "_acme-challenge" || rr.starts_with("_acme-challenge.") {
|
|
||||||
match delete_dns_record(client, &r.record_id) {
|
|
||||||
Err(e) => warning!("Delete dns: {}.{}, failed: {}", r.rr, r.domain_name, e),
|
|
||||||
Ok(Err(e)) => warning!("Delete dns: {}.{}, failed: {:?}", r.rr, r.domain_name, e),
|
|
||||||
Ok(Ok(_)) => success!("Delete dns: {}.{}", r.rr, r.domain_name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
match &ali_yun_client {
|
|
||||||
Some(client) => {
|
|
||||||
let add_txt_dns_result = opt_result!(add_txt_dns_record(client, &rr_and_domain.1, &rr_and_domain.0, &proof), "Add DNS TXT record failed: {}");
|
|
||||||
match add_txt_dns_result {
|
|
||||||
Ok(s) => success!("Add dns txt record successes: {}", s.record_id),
|
|
||||||
Err(e) => return simple_error!("Add dns txt record failed: {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => if acme_request.allow_interact {
|
|
||||||
let mut line = String::new();
|
|
||||||
information!("You need to config dns manually, press enter to continue...");
|
|
||||||
let _ = std::io::stdin().read_line(&mut line).unwrap();
|
|
||||||
} else {
|
|
||||||
return simple_error!("Interact is not allowed, --allow-interact to allow interact");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugging!("Valid acme certificate dns challenge");
|
|
||||||
opt_result!(chall.validate(acme_request.timeout), "Validate dns challenge failed: {}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugging!("Refresh acme certificate order");
|
|
||||||
opt_result!(ord_new.refresh(), "Refresh order failed: {}");
|
|
||||||
};
|
|
||||||
|
|
||||||
information!("Generate private key, key type: {:?}", acme_request.algo);
|
|
||||||
let pkey_pri = match acme_request.algo {
|
|
||||||
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp256r1) => create_p256_key(),
|
|
||||||
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp384r1) => create_p384_key(),
|
|
||||||
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp521r1) => return simple_error!("Algo ec521 is not supported"),
|
|
||||||
X509PublicKeyAlgo::Rsa(bits) => create_rsa_key(bits),
|
|
||||||
};
|
|
||||||
|
|
||||||
debugging!("Invoking csr finalize pkey");
|
|
||||||
let ord_cert = opt_result!( ord_csr.finalize_pkey(pkey_pri, acme_request.timeout), "Submit CSR failed: {}");
|
|
||||||
debugging!("Downloading and save cert");
|
|
||||||
let cert = opt_result!( ord_cert.download_and_save_cert(), "Download and save certificate failed: {}");
|
|
||||||
|
|
||||||
if let (Some(cert_file), Some(key_file)) = (&acme_request.cert_file, &acme_request.key_file) {
|
|
||||||
debugging!("Certificate key: {}", cert.private_key());
|
|
||||||
debugging!("Certificate pem: {}", cert.certificate());
|
|
||||||
information!("Write file: {}", cert_file);
|
|
||||||
if let Err(e) = fs::write(cert_file, cert.certificate()) {
|
|
||||||
failure!("Write file: {}, failed: {}", cert_file, e);
|
|
||||||
}
|
|
||||||
information!("Write file: {}", key_file);
|
|
||||||
if let Err(e) = fs::write(key_file, cert.private_key()) {
|
|
||||||
failure!("Write file: {}, failed: {}", key_file, e);
|
|
||||||
}
|
|
||||||
success!("Write files success: {} and {}", cert_file, key_file);
|
|
||||||
} else if let Some(outputs_file) = &acme_request.outputs_file {
|
|
||||||
let mut outputs = String::new();
|
|
||||||
outputs.push_str("private key:\n");
|
|
||||||
outputs.push_str(cert.private_key());
|
|
||||||
outputs.push_str("\n\ncertificates:\n");
|
|
||||||
outputs.push_str(cert.certificate());
|
|
||||||
if let Err(e) = fs::write(outputs_file, outputs) {
|
|
||||||
failure!("Write file: {}, failed: {}", outputs_file, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
information!("Certificate key: {}", cert.private_key());
|
|
||||||
information!("Certificate pem: {}", cert.certificate());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn startup_http_server(s: Sender<i32>, port: u16) {
|
fn startup_http_server(s: Sender<i32>, port: u16) {
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
information!("Listen at 0.0.0.0:{}", port);
|
information!("Listen at 0.0.0.0:{}", port);
|
||||||
@@ -608,7 +423,7 @@ fn startup_http_server(s: Sender<i32>, port: u16) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let peer = req.peer_addr().unwrap_or("none");
|
let peer = req.peer_addr().unwrap_or("none");
|
||||||
let auth_token = { TOKEN_MAP.read().unwrap().get(token).cloned() };
|
let auth_token = { crate::acme::TOKEN_MAP.read().unwrap().get(token).cloned() };
|
||||||
match auth_token {
|
match auth_token {
|
||||||
Some(auth_token) => {
|
Some(auth_token) => {
|
||||||
information!("Request acme challenge: {} -> {}, peer: {:?}", token, auth_token, peer);
|
information!("Request acme challenge: {} -> {}, peer: {:?}", token, auth_token, peer);
|
||||||
|
|||||||
106
src/notification.rs
Normal file
106
src/notification.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use rust_util::XResult;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use hmac::digest::FixedOutput;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use crate::config::CertConfig;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct InnerTextMessageText {
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct InnerTextMessage {
|
||||||
|
pub msgtype: String,
|
||||||
|
pub text: InnerTextMessageText,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum NotifyToken {
|
||||||
|
Dingtalk((String, String)),
|
||||||
|
Webhook(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_notify_message(cert_config: &CertConfig, message: &str) -> XResult<()> {
|
||||||
|
let notify_token = parse_notify_token(cert_config);
|
||||||
|
match notify_token {
|
||||||
|
None => { /* DO NOTHING */ }
|
||||||
|
Some(NotifyToken::Dingtalk((access_token, sec_token))) => {
|
||||||
|
inner_send_dingtalk_message(&access_token, &sec_token, message)?;
|
||||||
|
}
|
||||||
|
Some(NotifyToken::Webhook(webhook)) => {
|
||||||
|
inner_send_message(&webhook, message)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_send_dingtalk_message(access_token: &str, sec_token: &str, message: &str) -> XResult<()> {
|
||||||
|
let webhook_url = build_dingtalk_post_message_url(access_token, sec_token)?;
|
||||||
|
inner_send_message(&webhook_url, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_send_message(webhook_url: &str, message: &str) -> XResult<()> {
|
||||||
|
let message_json = serde_json::to_string(&InnerTextMessage {
|
||||||
|
msgtype: "text".into(),
|
||||||
|
text: InnerTextMessageText {
|
||||||
|
content: message.into(),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let response = client.post(webhook_url)
|
||||||
|
.header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
.body(message_json.as_bytes().to_vec())
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
information!("Send notify message: {:?}", response);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_dingtalk_post_message_url(access_token: &str, sec_token: &str) -> XResult<String> {
|
||||||
|
let mut webhook_url = "https://oapi.dingtalk.com/robot/send".to_string();
|
||||||
|
webhook_url.push_str("?access_token=");
|
||||||
|
webhook_url.push_str(&urlencoding::encode(access_token));
|
||||||
|
if !sec_token.is_empty() {
|
||||||
|
let timestamp = &format!("{}", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis());
|
||||||
|
let timestamp_and_secret = &format!("{}\n{}", timestamp, sec_token);
|
||||||
|
let hmac_sha256 = STANDARD.encode(&calc_hmac_sha256(sec_token.as_bytes(), timestamp_and_secret.as_bytes())?[..]);
|
||||||
|
webhook_url.push_str("×tamp=");
|
||||||
|
webhook_url.push_str(timestamp);
|
||||||
|
webhook_url.push_str("&sign=");
|
||||||
|
webhook_url.push_str(&urlencoding::encode(&hmac_sha256));
|
||||||
|
}
|
||||||
|
Ok(webhook_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// format: dingtalk:access_token?sec_token
|
||||||
|
#[allow(clippy::manual_map)]
|
||||||
|
fn parse_notify_token(cert_config: &CertConfig) -> Option<NotifyToken> {
|
||||||
|
let token = cert_config.notify_token.as_ref()?;
|
||||||
|
if let Some(token_and_or_sec) = token.strip_prefix("dingtalk:") {
|
||||||
|
let mut token_and_or_sec_vec = token_and_or_sec.split('?');
|
||||||
|
let access_token = token_and_or_sec_vec.next().unwrap_or(token_and_or_sec);
|
||||||
|
let sec_token = token_and_or_sec_vec.next().unwrap_or("");
|
||||||
|
Some(NotifyToken::Dingtalk((access_token.into(), sec_token.into())))
|
||||||
|
} else if let Some(webhook_url) = token.strip_prefix("webhook:") {
|
||||||
|
Some(NotifyToken::Webhook(webhook_url.into()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// calc hma_sha256 digest
|
||||||
|
fn calc_hmac_sha256(key: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
||||||
|
let mut mac = match Hmac::<Sha256>::new_from_slice(key) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(Box::new(Error::new(ErrorKind::Other, format!("Hmac error: {}", e))));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mac.update(message);
|
||||||
|
Ok(mac.finalize_fixed().to_vec())
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use rust_util::XResult;
|
use std::thread::{JoinHandle, spawn, sleep};
|
||||||
use std::thread;
|
|
||||||
use std::thread::JoinHandle;
|
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use rust_util::XResult;
|
||||||
|
|
||||||
pub struct SimpleThreadPool {
|
pub struct SimpleThreadPool {
|
||||||
max_pool_size: u32,
|
max_pool_size: u32,
|
||||||
@@ -28,7 +27,7 @@ impl SimpleThreadPool {
|
|||||||
let running = self.running_pool_size.fetch_add(1, Ordering::SeqCst);
|
let running = self.running_pool_size.fetch_add(1, Ordering::SeqCst);
|
||||||
let running_pool_size_clone = self.running_pool_size.clone();
|
let running_pool_size_clone = self.running_pool_size.clone();
|
||||||
if running < self.max_pool_size {
|
if running < self.max_pool_size {
|
||||||
Some(thread::spawn(move || {
|
Some(spawn(move || {
|
||||||
f();
|
f();
|
||||||
running_pool_size_clone.fetch_sub(1, Ordering::SeqCst);
|
running_pool_size_clone.fetch_sub(1, Ordering::SeqCst);
|
||||||
}))
|
}))
|
||||||
@@ -47,7 +46,7 @@ fn test_simple_thread_pool() {
|
|||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
if let Some(h) = stp.submit(move || {
|
if let Some(h) = stp.submit(move || {
|
||||||
println!("Task start: {}", i);
|
println!("Task start: {}", i);
|
||||||
thread::sleep(Duration::from_secs(1));
|
sleep(Duration::from_secs(1));
|
||||||
println!("Task end: {}", i);
|
println!("Task end: {}", i);
|
||||||
}) {
|
}) {
|
||||||
handlers.push(h);
|
handlers.push(h);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::time::SystemTime;
|
|||||||
use std::fmt::{Display, Formatter, Result};
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AcmeStatics {
|
pub struct AcmeStatistics {
|
||||||
pub started: SystemTime,
|
pub started: SystemTime,
|
||||||
pub ended: Option<SystemTime>,
|
pub ended: Option<SystemTime>,
|
||||||
pub items: Vec<AcmeItem>,
|
pub items: Vec<AcmeItem>,
|
||||||
@@ -36,14 +36,14 @@ impl Display for AcmeItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AcmeStatics {
|
impl Display for AcmeStatistics {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
let mut sb = String::with_capacity(512);
|
let mut sb = String::with_capacity(512);
|
||||||
let df = simpledateformat::fmt("yyyy-MM-dd HH:mm:ss z").unwrap();
|
let df = simpledateformat::fmt("yyyy-MM-dd HH:mm:ss z").unwrap();
|
||||||
let started_time = df.format_local(self.started.clone());
|
let started_time = df.format_local(self.started);
|
||||||
sb.push_str(&format!("Started: {}", &started_time));
|
sb.push_str(&format!("Started: {}", &started_time));
|
||||||
if let Some(ended) = &self.ended {
|
if let Some(ended) = &self.ended {
|
||||||
let ended_time = df.format_local(ended.clone());
|
let ended_time = df.format_local(*ended);
|
||||||
sb.push_str(&format!("\nEnded: {}", &ended_time));
|
sb.push_str(&format!("\nEnded: {}", &ended_time));
|
||||||
let cost_result = ended.duration_since(self.started);
|
let cost_result = ended.duration_since(self.started);
|
||||||
if let Ok(cost) = cost_result {
|
if let Ok(cost) = cost_result {
|
||||||
@@ -72,9 +72,9 @@ impl Display for AcmeStatics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcmeStatics {
|
impl AcmeStatistics {
|
||||||
pub fn start() -> AcmeStatics {
|
pub fn start() -> AcmeStatistics {
|
||||||
AcmeStatics {
|
AcmeStatistics {
|
||||||
started: SystemTime::now(),
|
started: SystemTime::now(),
|
||||||
ended: None,
|
ended: None,
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
use rust_util::XResult;
|
use rust_util::XResult;
|
||||||
|
|
||||||
|
// "example.com" -> ("@", "example.com")
|
||||||
|
// "www.example.com" -> ("www", "example.com")
|
||||||
pub fn parse_dns_record(record: &str) -> XResult<(String, String)> {
|
pub fn parse_dns_record(record: &str) -> XResult<(String, String)> {
|
||||||
let r = if record.ends_with(".") {
|
let r = if record.ends_with(".") {
|
||||||
record.chars().take(record.len() - 1).collect::<String>().to_ascii_lowercase()
|
record.chars().take(record.len() - 1).collect::<String>().to_ascii_lowercase()
|
||||||
@@ -18,7 +20,7 @@ pub fn parse_dns_record(record: &str) -> XResult<(String, String)> {
|
|||||||
// SHOULD read from: https://publicsuffix.org/
|
// SHOULD read from: https://publicsuffix.org/
|
||||||
let domain_parts_len = match last_part {
|
let domain_parts_len = match last_part {
|
||||||
"cn" => match last_part_2 {
|
"cn" => match last_part_2 {
|
||||||
"com" | "net" | "org" | "gov" => 3,
|
"com" | "net" | "org" | "gov" | "edu" => 3,
|
||||||
_ => 2,
|
_ => 2,
|
||||||
},
|
},
|
||||||
_ => 2,
|
_ => 2,
|
||||||
|
|||||||
33
src/x509.rs
33
src/x509.rs
@@ -1,19 +1,18 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use x509_parser::parse_x509_certificate;
|
|
||||||
use x509_parser::pem::parse_x509_pem;
|
|
||||||
use x509_parser::extensions::{ParsedExtension, GeneralName};
|
|
||||||
use x509_parser::der_parser::oid::Oid;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use rust_util::XResult;
|
|
||||||
use x509_parser::der_parser::parse_der;
|
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
|
||||||
use x509_parser::der_parser::ber::BerObjectContent;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use rust_util::XResult;
|
||||||
|
use x509_parser::{pem::parse_x509_pem, parse_x509_certificate};
|
||||||
|
use x509_parser::extensions::{ParsedExtension, GeneralName};
|
||||||
|
use x509_parser::der_parser::{ber::BerObjectContent, oid::Oid, parse_der};
|
||||||
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref OID_COMMON_NAME: Oid<'static> = Oid::from_str("2.5.4.3").unwrap();
|
static ref OID_COMMON_NAME: Oid<'static> = Oid::from_str("2.5.4.3").unwrap();
|
||||||
static ref OID_RSA_WITH_SHA256: Oid<'static> = Oid::from_str("1.2.840.113549.1.1.11").unwrap();
|
static ref OID_RSA_WITH_SHA256: Oid<'static> = Oid::from_str("1.2.840.113549.1.1.11").unwrap();
|
||||||
static ref OID_ECDSA_WITH_SHA256: Oid<'static> = Oid::from_str("1.2.840.10045.4.3.2").unwrap();
|
static ref OID_ECDSA_WITH_SHA256: Oid<'static> = Oid::from_str("1.2.840.10045.4.3.2").unwrap();
|
||||||
|
static ref OID_ECDSA_WITH_SHA384: Oid<'static> = Oid::from_str("1.2.840.10045.4.3.3").unwrap();
|
||||||
|
|
||||||
static ref OID_EC_PUBLIC_KEY: Oid<'static> = Oid::from_str("1.2.840.10045.2.1").unwrap();
|
static ref OID_EC_PUBLIC_KEY: Oid<'static> = Oid::from_str("1.2.840.10045.2.1").unwrap();
|
||||||
static ref OID_RSA_PUBLIC_KEY: Oid<'static> = Oid::from_str("1.2.840.113549.1.1.1").unwrap();
|
static ref OID_RSA_PUBLIC_KEY: Oid<'static> = Oid::from_str("1.2.840.113549.1.1.1").unwrap();
|
||||||
@@ -27,6 +26,7 @@ lazy_static! {
|
|||||||
pub enum X509IssuerAlgo {
|
pub enum X509IssuerAlgo {
|
||||||
RsaWithSha256,
|
RsaWithSha256,
|
||||||
EcdsaWithSha256,
|
EcdsaWithSha256,
|
||||||
|
EcdsaWithSha384,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -48,14 +48,15 @@ impl Default for X509PublicKeyAlgo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for X509PublicKeyAlgo {
|
impl Display for X509PublicKeyAlgo {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
let algo = match self {
|
||||||
Self::Rsa(bit_length) => format!("rsa{}", bit_length),
|
Self::Rsa(bit_length) => format!("rsa{}", bit_length),
|
||||||
Self::EcKey(X509EcPublicKeyAlgo::Secp256r1) => "p256".into(),
|
Self::EcKey(X509EcPublicKeyAlgo::Secp256r1) => "p256".into(),
|
||||||
Self::EcKey(X509EcPublicKeyAlgo::Secp384r1) => "p384".into(),
|
Self::EcKey(X509EcPublicKeyAlgo::Secp384r1) => "p384".into(),
|
||||||
Self::EcKey(X509EcPublicKeyAlgo::Secp521r1) => "p521".into(),
|
Self::EcKey(X509EcPublicKeyAlgo::Secp521r1) => "p521".into(),
|
||||||
}
|
};
|
||||||
|
write!(f, "{}", algo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ impl FromStr for X509PublicKeyAlgo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl X509PublicKeyAlgo {
|
impl X509PublicKeyAlgo {
|
||||||
pub fn parse<'a>(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'a>) -> XResult<Self> {
|
pub fn parse(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'_>) -> XResult<Self> {
|
||||||
let algorithm = &public_key_info.algorithm;
|
let algorithm = &public_key_info.algorithm;
|
||||||
let public_key_algo_oid = &algorithm.algorithm;
|
let public_key_algo_oid = &algorithm.algorithm;
|
||||||
if public_key_algo_oid == &*OID_EC_PUBLIC_KEY {
|
if public_key_algo_oid == &*OID_EC_PUBLIC_KEY {
|
||||||
@@ -145,6 +146,8 @@ pub fn parse_x509(pem_id: &str, pem: &str) -> XResult<X509Certificate> {
|
|||||||
X509IssuerAlgo::RsaWithSha256
|
X509IssuerAlgo::RsaWithSha256
|
||||||
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA256 {
|
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA256 {
|
||||||
X509IssuerAlgo::EcdsaWithSha256
|
X509IssuerAlgo::EcdsaWithSha256
|
||||||
|
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA384 {
|
||||||
|
X509IssuerAlgo::EcdsaWithSha384
|
||||||
} else {
|
} else {
|
||||||
return simple_error!("Parse pem: {}, unknown x509 algorithm oid: {:?}", pem_id, cert_algorithm_oid);
|
return simple_error!("Parse pem: {}, unknown x509 algorithm oid: {:?}", pem_id, cert_algorithm_oid);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user