diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8f18b..a1c313b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - The account key type and signature algorithm can now be specified in the configuration. - The delay to renew a certificate before its expiration date can be specified in the configuration using the `renew_delay` parameter at either the certificate, endpoint and global level. +- It is now possible to specify IP identifiers (RFC 8738). +- The hook templates of type `challenge-*` have a new `identifier_tls_alpn` field which contains, if available, the identifier in a form that is suitable to the TLS ALPN challenge. + +### Changed +- In the certificate configuration, the `domains` field has been renamed `identifiers`. +- The `domain` hook template variable has been renamed `identifier`. +- The default hooks have been updated. ### Fixed - The Makefile now works on FreeBSD. It should also work on other BSD although it has not been tested. diff --git a/README.md b/README.md index 04e5f85..735f8cc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ The Automatic Certificate Management Environment (ACME), is an internet standard ## Key features - http-01, dns-01 and [tls-alpn-01](https://tools.ietf.org/html/rfc8737) challenges +- IP Identifier Validation Extension [RFC 8738](https://tools.ietf.org/html/rfc8738) - RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates - Internationalized domain names support - Fully customizable challenge validation action @@ -35,7 +36,6 @@ The Automatic Certificate Management Environment (ACME), is an internet standard ## Planned features -- IP Identifier Validation Extension [RFC 8738](https://tools.ietf.org/html/rfc8738) - STAR Certificates [RFC 8739](https://tools.ietf.org/html/rfc8739) - Daemon and certificates management via the `acmectl` tool - Nonce scoping configuration diff --git a/acme_common/src/crypto/openssl_certificate.rs b/acme_common/src/crypto/openssl_certificate.rs index b061a31..9cef745 100644 --- a/acme_common/src/crypto/openssl_certificate.rs +++ b/acme_common/src/crypto/openssl_certificate.rs @@ -8,6 +8,7 @@ use openssl::stack::Stack; use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509}; use std::collections::HashSet; +use std::net::IpAddr; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const APP_ORG: &str = "ACMEd"; @@ -22,7 +23,7 @@ pub struct Csr { } impl Csr { - pub fn new(key_pair: &KeyPair, domains: &[String]) -> Result { + pub fn new(key_pair: &KeyPair, domains: &[String], ips: &[String]) -> Result { let mut builder = X509ReqBuilder::new()?; builder.set_pubkey(&key_pair.inner_key)?; let ctx = builder.x509v3_context(None); @@ -30,6 +31,9 @@ impl Csr { 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)?; @@ -83,8 +87,27 @@ impl X509Certificate { match self.inner_cert.subject_alt_names() { Some(s) => s .iter() - .filter(|v| v.dnsname().is_some()) - .map(|v| v.dnsname().unwrap().to_string()) + .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(), } diff --git a/acme_common/src/error.rs b/acme_common/src/error.rs index 03574fa..f59af98 100644 --- a/acme_common/src/error.rs +++ b/acme_common/src/error.rs @@ -45,6 +45,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: std::net::AddrParseError) -> Self { + format!("{}", error).into() + } +} + impl From for Error { fn from(error: std::string::FromUtf8Error) -> Self { format!("UTF-8 error: {}", error).into() diff --git a/acme_common/src/tests.rs b/acme_common/src/tests.rs index 77a27d4..fed6851 100644 --- a/acme_common/src/tests.rs +++ b/acme_common/src/tests.rs @@ -1,2 +1,3 @@ +mod certificate; mod crypto_keys; mod idna; diff --git a/acme_common/src/tests/certificate.rs b/acme_common/src/tests/certificate.rs new file mode 100644 index 0000000..d3cc1e2 --- /dev/null +++ b/acme_common/src/tests/certificate.rs @@ -0,0 +1,84 @@ +use crate::crypto::X509Certificate; +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-----"#; + +#[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); +} diff --git a/acmed/config/default_hooks.toml b/acmed/config/default_hooks.toml index 34e54e6..f6d4353 100644 --- a/acmed/config/default_hooks.toml +++ b/acmed/config/default_hooks.toml @@ -12,7 +12,7 @@ # -# http-01 challenge in "/var/www/{{domain}}/" +# http-01 challenge in "/var/www/{{identifier}}/" # [[hook]] @@ -21,7 +21,7 @@ type = ["challenge-http-01"] cmd = "mkdir" args = [ "-m", "0755", - "-p", "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge" + "-p", "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge" ] allow_failure = true @@ -30,7 +30,7 @@ name = "http-01-echo-echo" type = ["challenge-http-01"] cmd = "echo" args = ["{{proof}}"] -stdout = "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" +stdout = "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge/{{file_name}}" [[hook]] name = "http-01-echo-chmod" @@ -38,7 +38,7 @@ type = ["challenge-http-01"] cmd = "chmod" args = [ "a+r", - "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge/{{file_name}}" ] allow_failure = true @@ -48,7 +48,7 @@ type = ["challenge-http-01-clean"] cmd = "rm" args = [ "-f", - "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{#if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge/{{file_name}}" ] allow_failure = true @@ -71,10 +71,10 @@ name = "tls-alpn-01-tacd-start-tcp" type = ["challenge-tls-alpn-01"] cmd = "tacd" args = [ - "--pid-file", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", - "--domain", "{{domain}}", + "--pid-file", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{identifier}}.pid", + "--domain", "{{identifier_tls_alpn}}", "--acme-ext", "{{proof}}", - "--listen", "{{#if env.TACD_HOST}}{{env.TACD_HOST}}{{else}}{{domain}}{{/if}}:{{#if env.TACD_PORT}}{{env.TACD_PORT}}{{else}}5001{{/if}}" + "--listen", "{{#if env.TACD_HOST}}{{env.TACD_HOST}}{{else}}{{identifier}}{{/if}}:{{#if env.TACD_PORT}}{{env.TACD_PORT}}{{else}}5001{{/if}}" ] [[hook]] @@ -82,10 +82,10 @@ name = "tls-alpn-01-tacd-start-unix" type = ["challenge-tls-alpn-01"] cmd = "tacd" args = [ - "--pid-file", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", - "--domain", "{{domain}}", + "--pid-file", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{identifier}}.pid", + "--domain", "{{identifier_tls_alpn}}", "--acme-ext", "{{proof}}", - "--listen", "unix:{{#if env.TACD_SOCK_ROOT}}{{env.TACD_SOCK_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.sock" + "--listen", "unix:{{#if env.TACD_SOCK_ROOT}}{{env.TACD_SOCK_ROOT}}{{else}}/run{{/if}}/tacd_{{identifier}}.sock" ] [[hook]] @@ -93,7 +93,7 @@ name = "tls-alpn-01-tacd-kill" type = ["challenge-tls-alpn-01-clean"] cmd = "pkill" args = [ - "-F", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", + "-F", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{identifier}}.pid", ] allow_failure = true @@ -102,7 +102,7 @@ name = "tls-alpn-01-tacd-rm" type = ["challenge-tls-alpn-01-clean"] cmd = "rm" args = [ - "-f", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{domain}}.pid", + "-f", "{{#if env.TACD_PID_ROOT}}{{env.TACD_PID_ROOT}}{{else}}/run{{/if}}/tacd_{{identifier}}.pid", ] allow_failure = true diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index 349aff9..524e19a 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -4,6 +4,7 @@ use crate::acme_proto::structs::{ }; use crate::certificate::Certificate; use crate::endpoint::Endpoint; +use crate::identifier::IdentifierType; use crate::jws::encode_kid; use crate::storage; use acme_common::crypto::Csr; @@ -16,7 +17,7 @@ mod certificate; mod http; pub mod structs; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Challenge { Http01, Dns01, @@ -81,11 +82,6 @@ pub fn request_certificate( root_certs: &[String], endpoint: &mut Endpoint, ) -> Result<(), Error> { - let domains = cert - .domains - .iter() - .map(|d| d.dns.to_owned()) - .collect::>(); let mut hook_datas = vec![]; // Refresh the directory @@ -95,7 +91,7 @@ pub fn request_certificate( let account = AccountManager::new(endpoint, root_certs, cert)?; // Create a new order - let new_order = NewOrder::new(&domains); + let new_order = NewOrder::new(&cert.identifiers); let new_order = serde_json::to_string(&new_order)?; let data_builder = set_data_builder!(account, new_order.as_bytes()); let (order, order_url) = http::new_order(endpoint, root_certs, &data_builder)?; @@ -123,15 +119,16 @@ pub fn request_certificate( } // Fetch the associated challenges - let current_challenge = cert.get_domain_challenge(&auth.identifier.value)?; + 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 = challenge.get_proof(&account.key_pair)?; let file_name = challenge.get_file_name(); - let domain = auth.identifier.value.to_owned(); + 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, &domain)?; + let mut data = cert.call_challenge_hooks(&file_name, &proof, &identifier)?; data.0.is_clean_hook = true; hook_datas.push(data); @@ -162,9 +159,20 @@ pub fn request_certificate( // Finalize the order by sending the CSR let key_pair = certificate::get_key_pair(cert)?; - let domains: Vec = cert.domains.iter().map(|e| e.dns.to_owned()).collect(); + let domains: Vec = cert + .identifiers + .iter() + .filter(|e| e.id_type == IdentifierType::Dns) + .map(|e| e.value.to_owned()) + .collect(); + let ips: Vec = cert + .identifiers + .iter() + .filter(|e| e.id_type == IdentifierType::Ip) + .map(|e| e.value.to_owned()) + .collect(); let csr = json!({ - "csr": Csr::new(&key_pair, domains.as_slice())?.to_der_base64()?, + "csr": Csr::new(&key_pair, domains.as_slice(), ips.as_slice())?.to_der_base64()?, }); let csr = csr.to_string(); let data_builder = set_data_builder!(account, csr.as_bytes()); @@ -187,8 +195,8 @@ pub fn request_certificate( storage::write_certificate(cert, &crt.as_bytes())?; cert.info(&format!( - "Certificate renewed (domains: {})", - cert.domain_list() + "Certificate renewed (identifiers: {})", + cert.identifier_list() )); Ok(()) } diff --git a/acmed/src/acme_proto/structs.rs b/acmed/src/acme_proto/structs.rs index 49bfe73..8a8eedb 100644 --- a/acmed/src/acme_proto/structs.rs +++ b/acmed/src/acme_proto/structs.rs @@ -23,4 +23,4 @@ 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, IdentifierType, NewOrder, Order, OrderStatus}; +pub use order::{Identifier, NewOrder, Order, OrderStatus}; diff --git a/acmed/src/acme_proto/structs/authorization.rs b/acmed/src/acme_proto/structs/authorization.rs index 42b69b5..1b9a268 100644 --- a/acmed/src/acme_proto/structs/authorization.rs +++ b/acmed/src/acme_proto/structs/authorization.rs @@ -175,7 +175,7 @@ pub enum ChallengeStatus { #[cfg(test)] mod tests { use super::{Authorization, AuthorizationStatus, Challenge, ChallengeStatus}; - use crate::acme_proto::structs::IdentifierType; + use crate::identifier::IdentifierType; use std::str::FromStr; #[test] diff --git a/acmed/src/acme_proto/structs/order.rs b/acmed/src/acme_proto/structs/order.rs index 76146f1..5658351 100644 --- a/acmed/src/acme_proto/structs/order.rs +++ b/acmed/src/acme_proto/structs/order.rs @@ -1,4 +1,5 @@ use crate::acme_proto::structs::{ApiError, HttpApiError}; +use crate::identifier::{self, IdentifierType}; use acme_common::error::Error; use serde::{Deserialize, Serialize}; use std::fmt; @@ -12,9 +13,12 @@ pub struct NewOrder { } impl NewOrder { - pub fn new(domains: &[String]) -> Self { + pub fn new(identifiers: &[identifier::Identifier]) -> Self { NewOrder { - identifiers: domains.iter().map(|n| Identifier::new_dns(n)).collect(), + identifiers: identifiers + .iter() + .map(|n| Identifier::from_generic(n)) + .collect(), not_before: None, not_after: None, } @@ -74,10 +78,10 @@ pub struct Identifier { } impl Identifier { - pub fn new_dns(value: &str) -> Self { + pub fn from_generic(id: &identifier::Identifier) -> Self { Identifier { - id_type: IdentifierType::Dns, - value: value.to_string(), + id_type: id.id_type.to_owned(), + value: id.value.to_owned(), } } } @@ -90,21 +94,6 @@ impl fmt::Display for Identifier { } } -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] -pub enum IdentifierType { - #[serde(rename = "dns")] - Dns, -} - -impl fmt::Display for IdentifierType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = match self { - IdentifierType::Dns => "dns", - }; - write!(f, "{}", s) - } -} - #[cfg(test)] mod tests { use super::{Identifier, IdentifierType}; diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index d969aee..a869e22 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -1,6 +1,7 @@ use crate::acme_proto::Challenge; -use crate::config::{Account, Domain}; +use crate::config::Account; use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, HookType, PostOperationHookData}; +use crate::identifier::{Identifier, IdentifierType}; use crate::storage::{certificate_files_exists, get_certificate}; use acme_common::crypto::X509Certificate; use acme_common::error::Error; @@ -44,7 +45,7 @@ impl fmt::Display for Algorithm { #[derive(Clone, Debug)] pub struct Certificate { pub account: Account, - pub domains: Vec, + pub identifiers: Vec, pub algo: Algorithm, pub kp_reuse: bool, pub endpoint_name: String, @@ -88,17 +89,19 @@ impl Certificate { trace!("{}: {}", &self, msg); } - pub fn get_domain_challenge(&self, domain_name: &str) -> Result { - let domain_name = domain_name.to_string(); - for d in self.domains.iter() { - // strip wildcards from domain before matching - let base_domain = d.dns.trim_start_matches("*."); - if base_domain == domain_name { - let c = Challenge::from_str(&d.challenge)?; - return Ok(c); + pub fn get_identifier_from_str(&self, identifier: &str) -> Result { + 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!("{}: domain name not found", domain_name).into()) + Err(format!("{}: identifier not found", identifier).into()) } fn is_expiring(&self, cert: &X509Certificate) -> Result { @@ -110,12 +113,12 @@ impl Certificate { Ok(expires_in <= self.renew_delay) } - fn has_missing_domains(&self, cert: &X509Certificate) -> bool { + fn has_missing_identifiers(&self, cert: &X509Certificate) -> bool { let cert_names = cert.subject_alt_names(); let req_names = self - .domains + .identifiers .iter() - .map(|v| v.dns.to_owned()) + .map(|v| v.value.to_owned()) .collect::>(); let has_miss = req_names.difference(&cert_names).count() != 0; if has_miss { @@ -133,18 +136,18 @@ impl Certificate { } /// Return a comma-separated list of the domains this certificate is valid for. - pub fn domain_list(&self) -> String { - self.domains + pub fn identifier_list(&self) -> String { + self.identifiers .iter() - .map(|domain| &*domain.dns) + .map(|d| d.value.as_str()) .collect::>() .join(",") } pub fn should_renew(&self) -> Result { self.debug(&format!( - "Checking for renewal (domains: {})", - self.domain_list() + "Checking for renewal (identifiers: {})", + self.identifier_list() )); if !certificate_files_exists(&self) { self.debug("certificate does not exist: requesting one"); @@ -152,7 +155,7 @@ impl Certificate { } let cert = get_certificate(&self)?; - let renew = self.has_missing_domains(&cert); + let renew = self.has_missing_identifiers(&cert); let renew = renew || self.is_expiring(&cert)?; if renew { @@ -167,22 +170,21 @@ impl Certificate { &self, file_name: &str, proof: &str, - domain: &str, + identifier: &str, ) -> Result<(ChallengeHookData, HookType), Error> { - let challenge = self.get_domain_challenge(domain)?; + let identifier = self.get_identifier_from_str(identifier)?; let mut hook_data = ChallengeHookData { - challenge: challenge.to_string(), - domain: domain.to_string(), + 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(), is_clean_hook: false, env: HashMap::new(), }; hook_data.set_env(&self.env); - for d in self.domains.iter().filter(|d| d.dns == domain) { - hook_data.set_env(&d.env); - } - let hook_type = match challenge { + 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 => ( @@ -203,13 +205,13 @@ impl Certificate { } pub fn call_post_operation_hooks(&self, status: &str, is_success: bool) -> Result<(), Error> { - let domains = self - .domains + let identifiers = self + .identifiers .iter() - .map(|d| format!("{} ({})", d.dns, d.challenge)) + .map(|d| d.value.to_owned()) .collect::>(); let mut hook_data = PostOperationHookData { - domains, + identifiers, algorithm: self.algo.to_string(), status: status.to_string(), is_success, diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 58baa6e..fbd3cde 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -1,8 +1,8 @@ use crate::certificate::Algorithm; use crate::duration::parse_duration; use crate::hooks; +use crate::identifier::IdentifierType; use acme_common::error::Error; -use acme_common::to_idna; use log::info; use serde::Deserialize; use std::collections::HashMap; @@ -290,7 +290,7 @@ pub struct Account { pub struct Certificate { pub account: String, pub endpoint: String, - pub domains: Vec, + pub identifiers: Vec, pub algorithm: Option, pub kp_reuse: Option, pub directory: Option, @@ -321,12 +321,10 @@ impl Certificate { Algorithm::from_str(algo) } - pub fn get_domains(&self) -> Result, Error> { + pub fn get_identifiers(&self) -> Result, Error> { let mut ret = vec![]; - for d in self.domains.iter() { - let mut nd = d.clone(); - nd.dns = to_idna(&nd.dns)?; - ret.push(nd); + for id in self.identifiers.iter() { + ret.push(id.to_generic()?); } Ok(ret) } @@ -341,14 +339,16 @@ impl Certificate { pub fn get_crt_name(&self) -> Result { let name = match &self.name { Some(n) => n.to_string(), - None => self - .domains - .first() - .ok_or_else(|| Error::from("Certificate has no domain names."))? - .dns - .to_owned(), + None => { + let id = self + .identifiers + .first() + .ok_or_else(|| Error::from("Certificate has no identifiers."))?; + id.to_string() + } }; - Ok(name.replace("*", "_")) + let name = name.replace("*", "_").replace(":", "_"); + Ok(name) } pub fn get_crt_name_format(&self) -> String { @@ -408,16 +408,34 @@ impl Certificate { #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] -pub struct Domain { +pub struct Identifier { pub challenge: String, - pub dns: String, + pub dns: Option, + pub ip: Option, #[serde(default)] pub env: HashMap, } -impl fmt::Display for Domain { +impl fmt::Display for Identifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.dns) + let s = String::new(); + let msg = self.dns.as_ref().or_else(|| self.ip.as_ref()).unwrap_or(&s); + write!(f, "{}", msg) + } +} + +impl Identifier { + fn to_generic(&self) -> Result { + 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) } } diff --git a/acmed/src/hooks.rs b/acmed/src/hooks.rs index ad7e5c3..5138367 100644 --- a/acmed/src/hooks.rs +++ b/acmed/src/hooks.rs @@ -43,7 +43,7 @@ macro_rules! imple_hook_data_env { #[derive(Clone, Serialize)] pub struct PostOperationHookData { - pub domains: Vec, + pub identifiers: Vec, pub algorithm: String, pub status: String, pub is_success: bool, @@ -54,7 +54,8 @@ imple_hook_data_env!(PostOperationHookData); #[derive(Clone, Serialize)] pub struct ChallengeHookData { - pub domain: String, + pub identifier: String, + pub identifier_tls_alpn: String, pub challenge: String, pub file_name: String, pub proof: String, diff --git a/acmed/src/identifier.rs b/acmed/src/identifier.rs new file mode 100644 index 0000000..512efd2 --- /dev/null +++ b/acmed/src/identifier.rs @@ -0,0 +1,149 @@ +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!("{:x}.{:x}", first, second) +} + +#[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 { + 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, +} + +impl Identifier { + pub fn new( + id_type: IdentifierType, + value: &str, + challenge: &str, + env: &HashMap, + ) -> Result { + 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 {} cannot be used with identifier of type {}", + challenge, id_type + ); + return Err(msg.into()); + } + Ok(Identifier { + id_type, + value, + challenge, + env: env.clone(), + }) + } + + pub fn get_tls_alpn_name(&self) -> Result { + 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::>() + .join("."); + let dn = format!("{}.in-addr.arpa", dn); + Ok(dn) + } + IpAddr::V6(ip) => { + let dn = ip + .octets() + .iter() + .rev() + .map(u8_to_nibbles_string) + .collect::>() + .join("."); + let dn = format!("{}.ip6.arpa", dn); + 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" + ); + } +} diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 768cffa..d9af971 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -10,6 +10,7 @@ mod duration; mod endpoint; mod hooks; mod http; +mod identifier; mod jws; mod main_event_loop; mod storage; diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index b5af93c..00488fa 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -46,7 +46,7 @@ impl MainEventLoop { let endpoint_name = endpoint.name.clone(); let cert = Certificate { account: crt.get_account(&cnf)?, - domains: crt.get_domains()?, + identifiers: crt.get_identifiers()?, algo: crt.get_algorithm()?, kp_reuse: crt.get_kp_reuse(), endpoint_name: endpoint_name.clone(), diff --git a/man/en/acmed.8 b/man/en/acmed.8 index d074ddb..4645c53 100644 --- a/man/en/acmed.8 +++ b/man/en/acmed.8 @@ -76,6 +76,14 @@ configuration file. .%R RFC 8737 .%T Automated Certificate Management Environment (ACME) TLS Application-Layer Protocol Negotiation (ALPN) Challenge Extension .Re +.It +.Rs +.Rs +.%A R.B. Shoemaker +.%D February 2020 +.%R RFC 8738 +.%T Automated Certificate Management Environment (ACME) IP Identifier Validation Extension +.Re .Sh AUTHORS .An Rodolphe Bréard .Aq rodolphe@breard.tf diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index be1835e..176d388 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -190,7 +190,7 @@ The email address used to contact the account's holder. .It Ic certificate Array of table representing a certificate that will be requested to a CA. .Pp -Note that certificates are identified by the first domain in the list of domains. That means that if you reorder the domains so that a different domain is at the first position, a new certificate with a new name will be issued. +Note that certificates are identified by the first identifier in the list of identifiers. That means that if you reorder the identifiers so that a different identifier is at the first position, a new certificate with a new name will be issued. .Bl -tag .It Ic account Ar string Name of the account to use. @@ -198,11 +198,15 @@ Name of the account to use. Name of the endpoint to use. .It Ic env Ar table Table of environment variables that will be accessible from hooks. -.It Ic domains Ar array -Array of tables listing the domains that should be included in the certificate along with the challenge to use for each one. +.It Ic identifiers Ar array +Array of tables listing the identifiers that should be included in the certificate along with the challenge to use for each one. The +.Em dns +and +.Em ip +fields are mutually exclusive. .Bl -tag .It Ic challenge Ar string -The name of the challenge to use to prove the domain's ownership. Possible values are: +The name of the challenge to use to prove the identifier's ownership. Possible values are: .Bl -dash -compact .It http-01 @@ -213,6 +217,8 @@ tls-alpn-01 .El .It Ic dns Ar string The domain name. +.It Ic ip Ar string +The IP address. .It Ic env Ar table Table of environment variables that will be accessible from hooks. .El @@ -241,7 +247,7 @@ Period of time between the certificate renewal and its expiration date. The form section. Default is the value defined in the associated endpoint. .El .Sh WRITING A HOOK -When requesting a certificate from a CA using ACME, there are three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every domains to be included: It requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that use a given certificate, for the same reason. The last one is archiving: Although several default methods can be implemented, sometimes admins wants or are required to do it in a different way. +When requesting a certificate from a CA using ACME, there are three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every identifier to be included: it requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that use a given certificate, for the same reason. The last one is archiving: although several default methods can be implemented, sometimes admins wants or are required to do it in a different way. .Pp In order to allow full automation of the three above steps without imposing arbitrary restrictions or methods, .Xr acmed 8 @@ -271,7 +277,7 @@ specifications. The available types and the associated template variable are described below. .Bl -tag .It Ic challenge-http-01 -Invoked when the ownership of a domain must be proved using the +Invoked when the ownership of an identifier must be proved using the .Em http-01 challenge. The available template variables are: .Bl -tag -compact @@ -279,8 +285,10 @@ challenge. The available template variables are: The name of the challenge type .Aq http-01 . Mostly used in hooks with multiple types. -.It Cm domain Ar string -The domain name whom ownership is currently being validated. +.It Cm identifier Ar string +The identifier name whom ownership is currently being validated. +.It Cm identifier_tls_alpn Ar string +The identifier name whom ownership is currently being validated, in a form suitable for the TLS ALPN challenge. .It Cm env Ar array Array containing all the environment variables. .It Cm file_name Ar string @@ -294,7 +302,7 @@ The content of the proof that must be written to .Em file_name . .El .It Ic challenge-http-01-clean -Invoked once a domain ownership has been proven using the +Invoked once an identifier ownership has been proven using the .Em http-01 challenge. This hook is intended to remove the proof since it is no longer required. The template variables are strictly identical to those given in the corresponding .Em challenge-http-01 @@ -303,7 +311,7 @@ hook, excepted which is set to .Em true . .It Ic challenge-dns-01 -Invoked when the ownership of a domain must be proved using the +Invoked when the ownership of an identifier must be proved using the .Em dns-01 challenge. The available template variables are: .Bl -tag -compact @@ -311,8 +319,10 @@ challenge. The available template variables are: The name of the challenge type .Aq dns-01 . Mostly used in hooks with multiple types. -.It Cm domain Ar string -The domain name whom ownership is currently being validated. +.It Cm identifier Ar string +The identifier name whom ownership is currently being validated. +.It Cm identifier_tls_alpn Ar string +The identifier name whom ownership is currently being validated, in a form suitable for the TLS ALPN challenge. .It Cm env Ar array Array containing all the environment variables. .It Cm is_clean_hook Ar bool @@ -325,7 +335,7 @@ entry of the DNS zone for the subdomain. .El .It Ic challenge-dns-01-clean -Invoked once a domain ownership has been proven using the +Invoked once an identifier ownership has been proven using the .Em dns-01 challenge. This hook is intended to remove the proof since it is no longer required. The template variables are strictly identical to those given in the corresponding .Em challenge-dns-01 @@ -334,7 +344,7 @@ hook, excepted which is set to .Em true . .It Ic challenge-tls-alpn-01 -Invoked when the ownership of a domain must be proved using the +Invoked when the ownership of an identifier must be proved using the .Em tls-alpn-01 challenge. The available template variables are: .Bl -tag -compact @@ -342,8 +352,10 @@ challenge. The available template variables are: The name of the challenge type .Aq tls-alpn-01 . Mostly used in hooks with multiple types. -.It Cm domain Ar string -The domain name whom ownership is currently being validated. +.It Cm identifier Ar string +The identifier name whom ownership is currently being validated. +.It Cm identifier_tls_alpn Ar string +The identifier name whom ownership is currently being validated, in a form suitable for the TLS ALPN challenge. .It Cm env Ar array Array containing all the environment variables. .It Cm is_clean_hook Ar bool @@ -359,7 +371,7 @@ will not generate the certificate itself since it can be done using .Xr tacd 8 . .El .It Ic challenge-tls-alpn-01-clean -Invoked once a domain ownership has been proven using the +Invoked once an identifier ownership has been proven using the .Em tls-alpn-01 challenge. This hook is intended to remove the proof since it is no longer required. The template variables are strictly identical to those given in the corresponding .Em challenge-tls-alpn-01 @@ -412,8 +424,8 @@ Invoked at the end of the certificate request process. The available template va .Bl -tag -compact .It Cm algorithm Ar string Name of the algorithm used in the certificate. -.It Cm domains Ar string -Array containing the domain names included in the requested certificate. +.It Cm identifiers Ar string +Array containing the identifiers included in the requested certificate. .It Cm env Ar array Array containing all the environment variables. .It Cm is_success Ar boolean @@ -435,10 +447,10 @@ and environment variables. .It Pa http-01-echo This hook is designed to solve the http-01 challenge. For this purpose, it will write the proof into -.Pa {{env.HTTP_ROOT}}/{{domain}}/.well-known/acme-challenge/{{file_name}} . +.Pa {{env.HTTP_ROOT}}/{{identifier}}/.well-known/acme-challenge/{{file_name}} . .Pp The web server must be configured so the file -.Pa http://{{domain}}/.well-known/acme-challenge/{{file_name}} +.Pa http://{{identifier}}/.well-known/acme-challenge/{{file_name}} can be accessed from the CA. .Pp If @@ -457,13 +469,13 @@ option. .Xr tacd 8 will listen on the host defined by the .Ev TACD_HOST -environment variable (default is the domain to be validated) and on the port defined by the +environment variable (default is the identifier to be validated) and on the port defined by the .Ev TACD_PORT environment variable (default is 5001). .Pp .Xr tacd 8 will store its pid into -.Pa {{TACD_PID_ROOT}}/tacd_{{domain}}.pid . +.Pa {{TACD_PID_ROOT}}/tacd_{{identifier}}.pid . If .Ev TACD_PID_ROOT is not specified, it will be set to @@ -479,7 +491,7 @@ option. .Pp .Xr tacd 8 will listen on the unix socket -.Pa {{env.TACD_SOCK_ROOT}}/tacd_{{domain}}.sock . +.Pa {{env.TACD_SOCK_ROOT}}/tacd_{{identifier}}.sock . If .Ev TACD_SOCK_ROOT is not specified, it will be set to @@ -487,7 +499,7 @@ is not specified, it will be set to .Pp .Xr tacd 8 will store its pid into -.Pa {{TACD_PID_ROOT}}/tacd_{{domain}}.pid . +.Pa {{TACD_PID_ROOT}}/tacd_{{identifier}}.pid . If .Ev TACD_PID_ROOT is not specified, it will be set to @@ -537,7 +549,7 @@ Default accounts private and public keys directory. .It Pa /etc/acmed/certs Default certificates and associated private keys directory. .Sh EXAMPLES -The following example defines a typical endpoint, account and certificate for a domain and several subdomains. +The following example defines a typical endpoint, account and certificate for a domain, several subdomains and an IP address. .Bd -literal -offset indent [[endpoint]] name = "example name" @@ -551,11 +563,12 @@ email = "certs@exemple.net" [[certificate]] endpoint = "example name" account = "my test account" -domains = [ +identifiers = [ { dns = "exemple.net", challenge = "http-01"}, { dns = "1.exemple.net", challenge = "dns-01"}, { dns = "2.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5010"}, { dns = "3.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5011"}, + { ip = "203.0.113.1", challenge = "http-01"}, ] hooks = ["git", "http-01-echo", "tls-alpn-01-tacd-tcp", "some-dns-01-hook"] env.HTTP_ROOT = "/srv/http" @@ -579,7 +592,7 @@ type = ["challenge-http-01"] cmd = "mkdir" args = [ "-m", "0755", - "-p", "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge" + "-p", "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge" ] [[hook]] @@ -587,7 +600,7 @@ name = "http-01-echo-echo" type = ["challenge-http-01"] cmd = "echo" args = ["{{proof}}"] -stdout = "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" +stdout = "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge/{{file_name}}" [[hook]] name = "http-01-echo-chmod" @@ -595,7 +608,7 @@ type = ["challenge-http-01-clean"] cmd = "chmod" args = [ "a+r", - "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge/{{file_name}}" ] [[hook]] @@ -604,7 +617,7 @@ type = ["challenge-http-01-clean"] cmd = "rm" args = [ "-f", - "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{domain}}/.well-known/acme-challenge/{{file_name}}" + "{{%if env.HTTP_ROOT}}{{env.HTTP_ROOT}}{{else}}/var/www{{/if}}/{{identifier}}/.well-known/acme-challenge/{{file_name}}" ] .Ed .Pp @@ -638,10 +651,10 @@ args = [ "-f", "noreply.certs@example.net", "contact@example.net" ] -stdin_str = """Subject: Certificate renewal {{#if is_success}}succeeded{{else}}failed{{/if}} for {{domains.[0]}} +stdin_str = """Subject: Certificate renewal {{#if is_success}}succeeded{{else}}failed{{/if}} for {{identifiers.[0]}} The following certificate has {{#unless is_success}}*not* {{/unless}}been renewed. -domains: {{#each domains}}{{#if @index}}, {{/if}}{{this}}{{/each}} +identifiers: {{#each identifiers}}{{#if @index}}, {{/if}}{{this}}{{/each}} algorithm: {{algorithm}} status: {{status}}""" .Ed