Browse Source

Implement IP identifiers

RFC 8738: https://tools.ietf.org/html/rfc8738
pull/39/head
Rodolphe Breard 4 years ago
parent
commit
25450aebbf
  1. 7
      CHANGELOG.md
  2. 2
      README.md
  3. 29
      acme_common/src/crypto/openssl_certificate.rs
  4. 6
      acme_common/src/error.rs
  5. 1
      acme_common/src/tests.rs
  6. 84
      acme_common/src/tests/certificate.rs
  7. 26
      acmed/config/default_hooks.toml
  8. 36
      acmed/src/acme_proto.rs
  9. 2
      acmed/src/acme_proto/structs.rs
  10. 2
      acmed/src/acme_proto/structs/authorization.rs
  11. 29
      acmed/src/acme_proto/structs/order.rs
  12. 66
      acmed/src/certificate.rs
  13. 54
      acmed/src/config.rs
  14. 5
      acmed/src/hooks.rs
  15. 149
      acmed/src/identifier.rs
  16. 1
      acmed/src/main.rs
  17. 2
      acmed/src/main_event_loop.rs
  18. 8
      man/en/acmed.8
  19. 79
      man/en/acmed.toml.5

7
CHANGELOG.md

@ -18,6 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- The account key type and signature algorithm can now be specified in the configuration. - 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. - 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 ### Fixed
- The Makefile now works on FreeBSD. It should also work on other BSD although it has not been tested. - The Makefile now works on FreeBSD. It should also work on other BSD although it has not been tested.

2
README.md

@ -19,6 +19,7 @@ The Automatic Certificate Management Environment (ACME), is an internet standard
## Key features ## Key features
- http-01, dns-01 and [tls-alpn-01](https://tools.ietf.org/html/rfc8737) challenges - 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 - RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates
- Internationalized domain names support - Internationalized domain names support
- Fully customizable challenge validation action - Fully customizable challenge validation action
@ -35,7 +36,6 @@ The Automatic Certificate Management Environment (ACME), is an internet standard
## Planned features ## Planned features
- IP Identifier Validation Extension [RFC 8738](https://tools.ietf.org/html/rfc8738)
- STAR Certificates [RFC 8739](https://tools.ietf.org/html/rfc8739) - STAR Certificates [RFC 8739](https://tools.ietf.org/html/rfc8739)
- Daemon and certificates management via the `acmectl` tool - Daemon and certificates management via the `acmectl` tool
- Nonce scoping configuration - Nonce scoping configuration

29
acme_common/src/crypto/openssl_certificate.rs

@ -8,6 +8,7 @@ use openssl::stack::Stack;
use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509}; use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509};
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
const APP_ORG: &str = "ACMEd"; const APP_ORG: &str = "ACMEd";
@ -22,7 +23,7 @@ pub struct Csr {
} }
impl Csr { impl Csr {
pub fn new(key_pair: &KeyPair, domains: &[String]) -> Result<Self, Error> {
pub fn new(key_pair: &KeyPair, domains: &[String], ips: &[String]) -> Result<Self, Error> {
let mut builder = X509ReqBuilder::new()?; let mut builder = X509ReqBuilder::new()?;
builder.set_pubkey(&key_pair.inner_key)?; builder.set_pubkey(&key_pair.inner_key)?;
let ctx = builder.x509v3_context(None); let ctx = builder.x509v3_context(None);
@ -30,6 +31,9 @@ impl Csr {
for dns in domains.iter() { for dns in domains.iter() {
san.dns(&dns); san.dns(&dns);
} }
for ip in ips.iter() {
san.ip(&ip);
}
let san = san.build(&ctx)?; let san = san.build(&ctx)?;
let mut ext_stack = Stack::new()?; let mut ext_stack = Stack::new()?;
ext_stack.push(san)?; ext_stack.push(san)?;
@ -83,8 +87,27 @@ impl X509Certificate {
match self.inner_cert.subject_alt_names() { match self.inner_cert.subject_alt_names() {
Some(s) => s Some(s) => s
.iter() .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(), .collect(),
None => HashSet::new(), None => HashSet::new(),
} }

6
acme_common/src/error.rs

@ -45,6 +45,12 @@ impl From<std::io::Error> for Error {
} }
} }
impl From<std::net::AddrParseError> for Error {
fn from(error: std::net::AddrParseError) -> Self {
format!("{}", error).into()
}
}
impl From<std::string::FromUtf8Error> for Error { impl From<std::string::FromUtf8Error> for Error {
fn from(error: std::string::FromUtf8Error) -> Self { fn from(error: std::string::FromUtf8Error) -> Self {
format!("UTF-8 error: {}", error).into() format!("UTF-8 error: {}", error).into()

1
acme_common/src/tests.rs

@ -1,2 +1,3 @@
mod certificate;
mod crypto_keys; mod crypto_keys;
mod idna; mod idna;

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

26
acmed/config/default_hooks.toml

@ -12,7 +12,7 @@
# #
# http-01 challenge in "/var/www/{{domain}}/"
# http-01 challenge in "/var/www/{{identifier}}/"
# #
[[hook]] [[hook]]
@ -21,7 +21,7 @@ type = ["challenge-http-01"]
cmd = "mkdir" cmd = "mkdir"
args = [ args = [
"-m", "0755", "-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 allow_failure = true
@ -30,7 +30,7 @@ name = "http-01-echo-echo"
type = ["challenge-http-01"] type = ["challenge-http-01"]
cmd = "echo" cmd = "echo"
args = ["{{proof}}"] 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]] [[hook]]
name = "http-01-echo-chmod" name = "http-01-echo-chmod"
@ -38,7 +38,7 @@ type = ["challenge-http-01"]
cmd = "chmod" cmd = "chmod"
args = [ args = [
"a+r", "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 allow_failure = true
@ -48,7 +48,7 @@ type = ["challenge-http-01-clean"]
cmd = "rm" cmd = "rm"
args = [ args = [
"-f", "-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 allow_failure = true
@ -71,10 +71,10 @@ name = "tls-alpn-01-tacd-start-tcp"
type = ["challenge-tls-alpn-01"] type = ["challenge-tls-alpn-01"]
cmd = "tacd" cmd = "tacd"
args = [ 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}}", "--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]] [[hook]]
@ -82,10 +82,10 @@ name = "tls-alpn-01-tacd-start-unix"
type = ["challenge-tls-alpn-01"] type = ["challenge-tls-alpn-01"]
cmd = "tacd" cmd = "tacd"
args = [ 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}}", "--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]] [[hook]]
@ -93,7 +93,7 @@ name = "tls-alpn-01-tacd-kill"
type = ["challenge-tls-alpn-01-clean"] type = ["challenge-tls-alpn-01-clean"]
cmd = "pkill" cmd = "pkill"
args = [ 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 allow_failure = true
@ -102,7 +102,7 @@ name = "tls-alpn-01-tacd-rm"
type = ["challenge-tls-alpn-01-clean"] type = ["challenge-tls-alpn-01-clean"]
cmd = "rm" cmd = "rm"
args = [ 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 allow_failure = true

36
acmed/src/acme_proto.rs

@ -4,6 +4,7 @@ use crate::acme_proto::structs::{
}; };
use crate::certificate::Certificate; use crate::certificate::Certificate;
use crate::endpoint::Endpoint; use crate::endpoint::Endpoint;
use crate::identifier::IdentifierType;
use crate::jws::encode_kid; use crate::jws::encode_kid;
use crate::storage; use crate::storage;
use acme_common::crypto::Csr; use acme_common::crypto::Csr;
@ -16,7 +17,7 @@ mod certificate;
mod http; mod http;
pub mod structs; pub mod structs;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Challenge { pub enum Challenge {
Http01, Http01,
Dns01, Dns01,
@ -81,11 +82,6 @@ pub fn request_certificate(
root_certs: &[String], root_certs: &[String],
endpoint: &mut Endpoint, endpoint: &mut Endpoint,
) -> Result<(), Error> { ) -> Result<(), Error> {
let domains = cert
.domains
.iter()
.map(|d| d.dns.to_owned())
.collect::<Vec<String>>();
let mut hook_datas = vec![]; let mut hook_datas = vec![];
// Refresh the directory // Refresh the directory
@ -95,7 +91,7 @@ pub fn request_certificate(
let account = AccountManager::new(endpoint, root_certs, cert)?; let account = AccountManager::new(endpoint, root_certs, cert)?;
// Create a new order // 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 new_order = serde_json::to_string(&new_order)?;
let data_builder = set_data_builder!(account, new_order.as_bytes()); let data_builder = set_data_builder!(account, new_order.as_bytes());
let (order, order_url) = http::new_order(endpoint, root_certs, &data_builder)?; let (order, order_url) = http::new_order(endpoint, root_certs, &data_builder)?;
@ -123,15 +119,16 @@ pub fn request_certificate(
} }
// Fetch the associated challenges // 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() { for challenge in auth.challenges.iter() {
if current_challenge == *challenge { if current_challenge == *challenge {
let proof = challenge.get_proof(&account.key_pair)?; let proof = challenge.get_proof(&account.key_pair)?;
let file_name = challenge.get_file_name(); 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 // 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; data.0.is_clean_hook = true;
hook_datas.push(data); hook_datas.push(data);
@ -162,9 +159,20 @@ pub fn request_certificate(
// Finalize the order by sending the CSR // Finalize the order by sending the CSR
let key_pair = certificate::get_key_pair(cert)?; let key_pair = certificate::get_key_pair(cert)?;
let domains: Vec<String> = cert.domains.iter().map(|e| e.dns.to_owned()).collect();
let domains: Vec<String> = cert
.identifiers
.iter()
.filter(|e| e.id_type == IdentifierType::Dns)
.map(|e| e.value.to_owned())
.collect();
let ips: Vec<String> = cert
.identifiers
.iter()
.filter(|e| e.id_type == IdentifierType::Ip)
.map(|e| e.value.to_owned())
.collect();
let csr = json!({ 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 csr = csr.to_string();
let data_builder = set_data_builder!(account, csr.as_bytes()); 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())?; storage::write_certificate(cert, &crt.as_bytes())?;
cert.info(&format!( cert.info(&format!(
"Certificate renewed (domains: {})",
cert.domain_list()
"Certificate renewed (identifiers: {})",
cert.identifier_list()
)); ));
Ok(()) Ok(())
} }

2
acmed/src/acme_proto/structs.rs

@ -23,4 +23,4 @@ pub use authorization::{Authorization, AuthorizationStatus, Challenge};
pub use deserialize_from_str; pub use deserialize_from_str;
pub use directory::Directory; pub use directory::Directory;
pub use error::{AcmeError, ApiError, HttpApiError}; pub use error::{AcmeError, ApiError, HttpApiError};
pub use order::{Identifier, IdentifierType, NewOrder, Order, OrderStatus};
pub use order::{Identifier, NewOrder, Order, OrderStatus};

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

@ -175,7 +175,7 @@ pub enum ChallengeStatus {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Authorization, AuthorizationStatus, Challenge, ChallengeStatus}; use super::{Authorization, AuthorizationStatus, Challenge, ChallengeStatus};
use crate::acme_proto::structs::IdentifierType;
use crate::identifier::IdentifierType;
use std::str::FromStr; use std::str::FromStr;
#[test] #[test]

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

@ -1,4 +1,5 @@
use crate::acme_proto::structs::{ApiError, HttpApiError}; use crate::acme_proto::structs::{ApiError, HttpApiError};
use crate::identifier::{self, IdentifierType};
use acme_common::error::Error; use acme_common::error::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
@ -12,9 +13,12 @@ pub struct NewOrder {
} }
impl NewOrder { impl NewOrder {
pub fn new(domains: &[String]) -> Self {
pub fn new(identifiers: &[identifier::Identifier]) -> Self {
NewOrder { 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_before: None,
not_after: None, not_after: None,
} }
@ -74,10 +78,10 @@ pub struct Identifier {
} }
impl Identifier { impl Identifier {
pub fn new_dns(value: &str) -> Self {
pub fn from_generic(id: &identifier::Identifier) -> Self {
Identifier { 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)] #[cfg(test)]
mod tests { mod tests {
use super::{Identifier, IdentifierType}; use super::{Identifier, IdentifierType};

66
acmed/src/certificate.rs

@ -1,6 +1,7 @@
use crate::acme_proto::Challenge; 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::hooks::{self, ChallengeHookData, Hook, HookEnvData, HookType, PostOperationHookData};
use crate::identifier::{Identifier, IdentifierType};
use crate::storage::{certificate_files_exists, get_certificate}; use crate::storage::{certificate_files_exists, get_certificate};
use acme_common::crypto::X509Certificate; use acme_common::crypto::X509Certificate;
use acme_common::error::Error; use acme_common::error::Error;
@ -44,7 +45,7 @@ impl fmt::Display for Algorithm {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Certificate { pub struct Certificate {
pub account: Account, pub account: Account,
pub domains: Vec<Domain>,
pub identifiers: Vec<Identifier>,
pub algo: Algorithm, pub algo: Algorithm,
pub kp_reuse: bool, pub kp_reuse: bool,
pub endpoint_name: String, pub endpoint_name: String,
@ -88,17 +89,19 @@ impl Certificate {
trace!("{}: {}", &self, msg); trace!("{}: {}", &self, msg);
} }
pub fn get_domain_challenge(&self, domain_name: &str) -> Result<Challenge, Error> {
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<Identifier, Error> {
let identifier = identifier.to_string();
for d in self.identifiers.iter() {
let val = match d.id_type {
// strip wildcards from domain before matching
IdentifierType::Dns => d.value.trim_start_matches("*.").to_string(),
IdentifierType::Ip => d.value.to_owned(),
};
if identifier == val {
return Ok(d.clone());
} }
} }
Err(format!("{}: domain name not found", domain_name).into())
Err(format!("{}: identifier not found", identifier).into())
} }
fn is_expiring(&self, cert: &X509Certificate) -> Result<bool, Error> { fn is_expiring(&self, cert: &X509Certificate) -> Result<bool, Error> {
@ -110,12 +113,12 @@ impl Certificate {
Ok(expires_in <= self.renew_delay) 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 cert_names = cert.subject_alt_names();
let req_names = self let req_names = self
.domains
.identifiers
.iter() .iter()
.map(|v| v.dns.to_owned())
.map(|v| v.value.to_owned())
.collect::<HashSet<String>>(); .collect::<HashSet<String>>();
let has_miss = req_names.difference(&cert_names).count() != 0; let has_miss = req_names.difference(&cert_names).count() != 0;
if has_miss { if has_miss {
@ -133,18 +136,18 @@ impl Certificate {
} }
/// Return a comma-separated list of the domains this certificate is valid for. /// 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() .iter()
.map(|domain| &*domain.dns)
.map(|d| d.value.as_str())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join(",") .join(",")
} }
pub fn should_renew(&self) -> Result<bool, Error> { pub fn should_renew(&self) -> Result<bool, Error> {
self.debug(&format!( self.debug(&format!(
"Checking for renewal (domains: {})",
self.domain_list()
"Checking for renewal (identifiers: {})",
self.identifier_list()
)); ));
if !certificate_files_exists(&self) { if !certificate_files_exists(&self) {
self.debug("certificate does not exist: requesting one"); self.debug("certificate does not exist: requesting one");
@ -152,7 +155,7 @@ impl Certificate {
} }
let cert = get_certificate(&self)?; 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)?; let renew = renew || self.is_expiring(&cert)?;
if renew { if renew {
@ -167,22 +170,21 @@ impl Certificate {
&self, &self,
file_name: &str, file_name: &str,
proof: &str, proof: &str,
domain: &str,
identifier: &str,
) -> Result<(ChallengeHookData, HookType), Error> { ) -> Result<(ChallengeHookData, HookType), Error> {
let challenge = self.get_domain_challenge(domain)?;
let identifier = self.get_identifier_from_str(identifier)?;
let mut hook_data = ChallengeHookData { 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(), file_name: file_name.to_string(),
proof: proof.to_string(), proof: proof.to_string(),
is_clean_hook: false, is_clean_hook: false,
env: HashMap::new(), env: HashMap::new(),
}; };
hook_data.set_env(&self.env); 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::Http01 => (HookType::ChallengeHttp01, HookType::ChallengeHttp01Clean),
Challenge::Dns01 => (HookType::ChallengeDns01, HookType::ChallengeDns01Clean), Challenge::Dns01 => (HookType::ChallengeDns01, HookType::ChallengeDns01Clean),
Challenge::TlsAlpn01 => ( Challenge::TlsAlpn01 => (
@ -203,13 +205,13 @@ impl Certificate {
} }
pub fn call_post_operation_hooks(&self, status: &str, is_success: bool) -> Result<(), Error> { pub fn call_post_operation_hooks(&self, status: &str, is_success: bool) -> Result<(), Error> {
let domains = self
.domains
let identifiers = self
.identifiers
.iter() .iter()
.map(|d| format!("{} ({})", d.dns, d.challenge))
.map(|d| d.value.to_owned())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let mut hook_data = PostOperationHookData { let mut hook_data = PostOperationHookData {
domains,
identifiers,
algorithm: self.algo.to_string(), algorithm: self.algo.to_string(),
status: status.to_string(), status: status.to_string(),
is_success, is_success,

54
acmed/src/config.rs

@ -1,8 +1,8 @@
use crate::certificate::Algorithm; use crate::certificate::Algorithm;
use crate::duration::parse_duration; use crate::duration::parse_duration;
use crate::hooks; use crate::hooks;
use crate::identifier::IdentifierType;
use acme_common::error::Error; use acme_common::error::Error;
use acme_common::to_idna;
use log::info; use log::info;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
@ -290,7 +290,7 @@ pub struct Account {
pub struct Certificate { pub struct Certificate {
pub account: String, pub account: String,
pub endpoint: String, pub endpoint: String,
pub domains: Vec<Domain>,
pub identifiers: Vec<Identifier>,
pub algorithm: Option<String>, pub algorithm: Option<String>,
pub kp_reuse: Option<bool>, pub kp_reuse: Option<bool>,
pub directory: Option<String>, pub directory: Option<String>,
@ -321,12 +321,10 @@ impl Certificate {
Algorithm::from_str(algo) Algorithm::from_str(algo)
} }
pub fn get_domains(&self) -> Result<Vec<Domain>, Error> {
pub fn get_identifiers(&self) -> Result<Vec<crate::identifier::Identifier>, Error> {
let mut ret = vec![]; 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) Ok(ret)
} }
@ -341,14 +339,16 @@ impl Certificate {
pub fn get_crt_name(&self) -> Result<String, Error> { pub fn get_crt_name(&self) -> Result<String, Error> {
let name = match &self.name { let name = match &self.name {
Some(n) => n.to_string(), 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 { pub fn get_crt_name_format(&self) -> String {
@ -408,16 +408,34 @@ impl Certificate {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct Domain {
pub struct Identifier {
pub challenge: String, pub challenge: String,
pub dns: String,
pub dns: Option<String>,
pub ip: Option<String>,
#[serde(default)] #[serde(default)]
pub env: HashMap<String, String>, pub env: HashMap<String, String>,
} }
impl fmt::Display for Domain {
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 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<crate::identifier::Identifier, Error> {
let (t, v) = match &self.dns {
Some(d) => (IdentifierType::Dns, d),
None => match &self.ip {
Some(ip) => (IdentifierType::Ip, ip),
None => {
return Err("No identifier found.".into());
}
},
};
crate::identifier::Identifier::new(t, &v, &self.challenge, &self.env)
} }
} }

5
acmed/src/hooks.rs

@ -43,7 +43,7 @@ macro_rules! imple_hook_data_env {
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
pub struct PostOperationHookData { pub struct PostOperationHookData {
pub domains: Vec<String>,
pub identifiers: Vec<String>,
pub algorithm: String, pub algorithm: String,
pub status: String, pub status: String,
pub is_success: bool, pub is_success: bool,
@ -54,7 +54,8 @@ imple_hook_data_env!(PostOperationHookData);
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
pub struct ChallengeHookData { pub struct ChallengeHookData {
pub domain: String,
pub identifier: String,
pub identifier_tls_alpn: String,
pub challenge: String, pub challenge: String,
pub file_name: String, pub file_name: String,
pub proof: String, pub proof: String,

149
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<Challenge> {
match self {
IdentifierType::Dns => vec![Challenge::Http01, Challenge::Dns01, Challenge::TlsAlpn01],
IdentifierType::Ip => vec![Challenge::Http01, Challenge::TlsAlpn01],
}
}
}
impl fmt::Display for IdentifierType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
IdentifierType::Dns => "dns",
IdentifierType::Ip => "ip",
};
write!(f, "{}", name)
}
}
#[derive(Clone, Debug)]
pub struct Identifier {
pub id_type: IdentifierType,
pub value: String,
pub challenge: Challenge,
pub env: HashMap<String, String>,
}
impl Identifier {
pub fn new(
id_type: IdentifierType,
value: &str,
challenge: &str,
env: &HashMap<String, String>,
) -> Result<Self, Error> {
let value = match id_type {
IdentifierType::Dns => to_idna(value)?,
IdentifierType::Ip => IpAddr::from_str(value)?.to_string(),
};
let challenge = Challenge::from_str(challenge)?;
if !id_type.supported_challenges().contains(&challenge) {
let msg = format!(
"Challenge {} 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<String, Error> {
match &self.id_type {
IdentifierType::Dns => Ok(self.value.to_owned()),
IdentifierType::Ip => match IpAddr::from_str(&self.value)? {
IpAddr::V4(ip) => {
let dn = ip
.octets()
.iter()
.rev()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(".");
let dn = format!("{}.in-addr.arpa", dn);
Ok(dn)
}
IpAddr::V6(ip) => {
let dn = ip
.octets()
.iter()
.rev()
.map(u8_to_nibbles_string)
.collect::<Vec<String>>()
.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"
);
}
}

1
acmed/src/main.rs

@ -10,6 +10,7 @@ mod duration;
mod endpoint; mod endpoint;
mod hooks; mod hooks;
mod http; mod http;
mod identifier;
mod jws; mod jws;
mod main_event_loop; mod main_event_loop;
mod storage; mod storage;

2
acmed/src/main_event_loop.rs

@ -46,7 +46,7 @@ impl MainEventLoop {
let endpoint_name = endpoint.name.clone(); let endpoint_name = endpoint.name.clone();
let cert = Certificate { let cert = Certificate {
account: crt.get_account(&cnf)?, account: crt.get_account(&cnf)?,
domains: crt.get_domains()?,
identifiers: crt.get_identifiers()?,
algo: crt.get_algorithm()?, algo: crt.get_algorithm()?,
kp_reuse: crt.get_kp_reuse(), kp_reuse: crt.get_kp_reuse(),
endpoint_name: endpoint_name.clone(), endpoint_name: endpoint_name.clone(),

8
man/en/acmed.8

@ -76,6 +76,14 @@ configuration file.
.%R RFC 8737 .%R RFC 8737
.%T Automated Certificate Management Environment (ACME) TLS Application-Layer Protocol Negotiation (ALPN) Challenge Extension .%T Automated Certificate Management Environment (ACME) TLS Application-Layer Protocol Negotiation (ALPN) Challenge Extension
.Re .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 .Sh AUTHORS
.An Rodolphe Bréard .An Rodolphe Bréard
.Aq rodolphe@breard.tf .Aq rodolphe@breard.tf

79
man/en/acmed.toml.5

@ -190,7 +190,7 @@ The email address used to contact the account's holder.
.It Ic certificate .It Ic certificate
Array of table representing a certificate that will be requested to a CA. Array of table representing a certificate that will be requested to a CA.
.Pp .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 .Bl -tag
.It Ic account Ar string .It Ic account Ar string
Name of the account to use. Name of the account to use.
@ -198,11 +198,15 @@ Name of the account to use.
Name of the endpoint to use. Name of the endpoint to use.
.It Ic env Ar table .It Ic env Ar table
Table of environment variables that will be accessible from hooks. 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 .Bl -tag
.It Ic challenge Ar string .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 .Bl -dash -compact
.It .It
http-01 http-01
@ -213,6 +217,8 @@ tls-alpn-01
.El .El
.It Ic dns Ar string .It Ic dns Ar string
The domain name. The domain name.
.It Ic ip Ar string
The IP address.
.It Ic env Ar table .It Ic env Ar table
Table of environment variables that will be accessible from hooks. Table of environment variables that will be accessible from hooks.
.El .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. section. Default is the value defined in the associated endpoint.
.El .El
.Sh WRITING A HOOK .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 .Pp
In order to allow full automation of the three above steps without imposing arbitrary restrictions or methods, In order to allow full automation of the three above steps without imposing arbitrary restrictions or methods,
.Xr acmed 8 .Xr acmed 8
@ -271,7 +277,7 @@ specifications.
The available types and the associated template variable are described below. The available types and the associated template variable are described below.
.Bl -tag .Bl -tag
.It Ic challenge-http-01 .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 .Em http-01
challenge. The available template variables are: challenge. The available template variables are:
.Bl -tag -compact .Bl -tag -compact
@ -279,8 +285,10 @@ challenge. The available template variables are:
The name of the challenge type The name of the challenge type
.Aq http-01 . .Aq http-01 .
Mostly used in hooks with multiple types. 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 .It Cm env Ar array
Array containing all the environment variables. Array containing all the environment variables.
.It Cm file_name Ar string .It Cm file_name Ar string
@ -294,7 +302,7 @@ The content of the proof that must be written to
.Em file_name . .Em file_name .
.El .El
.It Ic challenge-http-01-clean .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 .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 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 .Em challenge-http-01
@ -303,7 +311,7 @@ hook, excepted
which is set to which is set to
.Em true . .Em true .
.It Ic challenge-dns-01 .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 .Em dns-01
challenge. The available template variables are: challenge. The available template variables are:
.Bl -tag -compact .Bl -tag -compact
@ -311,8 +319,10 @@ challenge. The available template variables are:
The name of the challenge type The name of the challenge type
.Aq dns-01 . .Aq dns-01 .
Mostly used in hooks with multiple types. 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 .It Cm env Ar array
Array containing all the environment variables. Array containing all the environment variables.
.It Cm is_clean_hook Ar bool .It Cm is_clean_hook Ar bool
@ -325,7 +335,7 @@ entry of the DNS zone for the
subdomain. subdomain.
.El .El
.It Ic challenge-dns-01-clean .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 .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 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 .Em challenge-dns-01
@ -334,7 +344,7 @@ hook, excepted
which is set to which is set to
.Em true . .Em true .
.It Ic challenge-tls-alpn-01 .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 .Em tls-alpn-01
challenge. The available template variables are: challenge. The available template variables are:
.Bl -tag -compact .Bl -tag -compact
@ -342,8 +352,10 @@ challenge. The available template variables are:
The name of the challenge type The name of the challenge type
.Aq tls-alpn-01 . .Aq tls-alpn-01 .
Mostly used in hooks with multiple types. 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 .It Cm env Ar array
Array containing all the environment variables. Array containing all the environment variables.
.It Cm is_clean_hook Ar bool .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 . .Xr tacd 8 .
.El .El
.It Ic challenge-tls-alpn-01-clean .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 .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 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 .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 .Bl -tag -compact
.It Cm algorithm Ar string .It Cm algorithm Ar string
Name of the algorithm used in the certificate. 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 .It Cm env Ar array
Array containing all the environment variables. Array containing all the environment variables.
.It Cm is_success Ar boolean .It Cm is_success Ar boolean
@ -435,10 +447,10 @@ and
environment variables. environment variables.
.It Pa http-01-echo .It Pa http-01-echo
This hook is designed to solve the http-01 challenge. For this purpose, it will write the proof into 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 .Pp
The web server must be configured so the file 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. can be accessed from the CA.
.Pp .Pp
If If
@ -457,13 +469,13 @@ option.
.Xr tacd 8 .Xr tacd 8
will listen on the host defined by the will listen on the host defined by the
.Ev TACD_HOST .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 .Ev TACD_PORT
environment variable (default is 5001). environment variable (default is 5001).
.Pp .Pp
.Xr tacd 8 .Xr tacd 8
will store its pid into will store its pid into
.Pa {{TACD_PID_ROOT}}/tacd_{{domain}}.pid .
.Pa {{TACD_PID_ROOT}}/tacd_{{identifier}}.pid .
If If
.Ev TACD_PID_ROOT .Ev TACD_PID_ROOT
is not specified, it will be set to is not specified, it will be set to
@ -479,7 +491,7 @@ option.
.Pp .Pp
.Xr tacd 8 .Xr tacd 8
will listen on the unix socket will listen on the unix socket
.Pa {{env.TACD_SOCK_ROOT}}/tacd_{{domain}}.sock .
.Pa {{env.TACD_SOCK_ROOT}}/tacd_{{identifier}}.sock .
If If
.Ev TACD_SOCK_ROOT .Ev TACD_SOCK_ROOT
is not specified, it will be set to is not specified, it will be set to
@ -487,7 +499,7 @@ is not specified, it will be set to
.Pp .Pp
.Xr tacd 8 .Xr tacd 8
will store its pid into will store its pid into
.Pa {{TACD_PID_ROOT}}/tacd_{{domain}}.pid .
.Pa {{TACD_PID_ROOT}}/tacd_{{identifier}}.pid .
If If
.Ev TACD_PID_ROOT .Ev TACD_PID_ROOT
is not specified, it will be set to is not specified, it will be set to
@ -537,7 +549,7 @@ Default accounts private and public keys directory.
.It Pa /etc/acmed/certs .It Pa /etc/acmed/certs
Default certificates and associated private keys directory. Default certificates and associated private keys directory.
.Sh EXAMPLES .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 .Bd -literal -offset indent
[[endpoint]] [[endpoint]]
name = "example name" name = "example name"
@ -551,11 +563,12 @@ email = "certs@exemple.net"
[[certificate]] [[certificate]]
endpoint = "example name" endpoint = "example name"
account = "my test account" account = "my test account"
domains = [
identifiers = [
{ dns = "exemple.net", challenge = "http-01"}, { dns = "exemple.net", challenge = "http-01"},
{ dns = "1.exemple.net", challenge = "dns-01"}, { dns = "1.exemple.net", challenge = "dns-01"},
{ dns = "2.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5010"}, { dns = "2.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5010"},
{ dns = "3.exemple.net", challenge = "tls-alpn-01", env.TACD_PORT="5011"}, { 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"] hooks = ["git", "http-01-echo", "tls-alpn-01-tacd-tcp", "some-dns-01-hook"]
env.HTTP_ROOT = "/srv/http" env.HTTP_ROOT = "/srv/http"
@ -579,7 +592,7 @@ type = ["challenge-http-01"]
cmd = "mkdir" cmd = "mkdir"
args = [ args = [
"-m", "0755", "-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]] [[hook]]
@ -587,7 +600,7 @@ name = "http-01-echo-echo"
type = ["challenge-http-01"] type = ["challenge-http-01"]
cmd = "echo" cmd = "echo"
args = ["{{proof}}"] 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]] [[hook]]
name = "http-01-echo-chmod" name = "http-01-echo-chmod"
@ -595,7 +608,7 @@ type = ["challenge-http-01-clean"]
cmd = "chmod" cmd = "chmod"
args = [ args = [
"a+r", "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]] [[hook]]
@ -604,7 +617,7 @@ type = ["challenge-http-01-clean"]
cmd = "rm" cmd = "rm"
args = [ args = [
"-f", "-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 .Ed
.Pp .Pp
@ -638,10 +651,10 @@ args = [
"-f", "noreply.certs@example.net", "-f", "noreply.certs@example.net",
"contact@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. 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}} algorithm: {{algorithm}}
status: {{status}}""" status: {{status}}"""
.Ed .Ed

Loading…
Cancel
Save