From 39430009acd4ae424059e8513eaf5b93d265896b Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Thu, 12 Mar 2020 19:55:47 +0100 Subject: [PATCH] Add internationalized domain names support --- CHANGELOG.md | 2 ++ acme_common/Cargo.toml | 1 + acme_common/src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++ acmed/src/config.rs | 11 +++++++ acmed/src/main_event_loop.rs | 2 +- tacd/src/main.rs | 2 ++ 6 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a0170..f5414ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Wildcard certificates are now supported. In the file name, the `*` is replaced by `_`. +- Internationalized domain names are now supported. ### Changed - The PID file is now always written whether or not ACMEd is running in the foreground. Previously, it was written only when running in the background. @@ -24,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - In the directory, the `externalAccountRequired` field is now a boolean instead of a string. + ## [0.6.1] - 2019-09-13 ### Fixed diff --git a/acme_common/Cargo.toml b/acme_common/Cargo.toml index 6f5158a..011af60 100644 --- a/acme_common/Cargo.toml +++ b/acme_common/Cargo.toml @@ -20,6 +20,7 @@ handlebars = "3.0" http_req = "0.5" log = "0.4" openssl = "0.10" +punycode = "0.4" serde_json = "1.0" syslog = "4.0" toml = "0.5" diff --git a/acme_common/src/lib.rs b/acme_common/src/lib.rs index 9dfb104..0e6b3aa 100644 --- a/acme_common/src/lib.rs +++ b/acme_common/src/lib.rs @@ -19,6 +19,23 @@ macro_rules! exit_match { }; } +pub fn to_idna(domain_name: &str) -> Result { + let mut idna_parts = vec![]; + let parts: Vec<&str> = domain_name.split('.').collect(); + for name in parts.iter() { + let raw_name = name.to_lowercase(); + let idna_name = if name.is_ascii() { + raw_name + } else { + let idna_name = punycode::encode(&raw_name) + .map_err(|_| error::Error::from("IDNA encoding failed."))?; + format!("xn--{}", idna_name) + }; + idna_parts.push(idna_name); + } + Ok(idna_parts.join(".")) +} + pub fn b64_encode>(input: &T) -> String { base64::encode_config(input, base64::URL_SAFE_NO_PAD) } @@ -39,3 +56,49 @@ fn write_pid_file(pid_file: &str) -> Result<(), error::Error> { file.sync_all()?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::to_idna; + + #[test] + fn test_no_idna() { + let idna_res = to_idna("HeLo.example.com"); + assert!(idna_res.is_ok()); + assert_eq!(idna_res.unwrap(), "helo.example.com"); + } + + #[test] + fn test_simple_idna() { + let idna_res = to_idna("Hélo.Example.com"); + assert!(idna_res.is_ok()); + assert_eq!(idna_res.unwrap(), "xn--hlo-bma.example.com"); + } + + #[test] + fn test_multiple_idna() { + let idna_res = to_idna("ns1.hÉlo.aç-éièè.example.com"); + assert!(idna_res.is_ok()); + assert_eq!( + idna_res.unwrap(), + "ns1.xn--hlo-bma.xn--a-i-2lahae.example.com" + ); + } + + #[test] + fn test_already_idna() { + let idna_res = to_idna("xn--hlo-bma.example.com"); + assert!(idna_res.is_ok()); + assert_eq!(idna_res.unwrap(), "xn--hlo-bma.example.com"); + } + + #[test] + fn test_mixed_idna_parts() { + let idna_res = to_idna("ns1.xn--hlo-bma.aç-éièè.example.com"); + assert!(idna_res.is_ok()); + assert_eq!( + idna_res.unwrap(), + "ns1.xn--hlo-bma.xn--a-i-2lahae.example.com" + ); + } +} diff --git a/acmed/src/config.rs b/acmed/src/config.rs index b1b9846..d36ea85 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -2,6 +2,7 @@ use crate::certificate::Algorithm; use crate::hooks; use crate::rate_limits; use acme_common::error::Error; +use acme_common::to_idna; use log::info; use serde::Deserialize; use std::collections::HashMap; @@ -289,6 +290,16 @@ impl Certificate { Algorithm::from_str(algo) } + pub fn get_domains(&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); + } + Ok(ret) + } + pub fn get_kp_reuse(&self) -> bool { match self.kp_reuse { Some(b) => b, diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 3ac39b7..b3e1238 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -48,7 +48,7 @@ impl MainEventLoop { .to_owned(); let cert = Certificate { account: crt.get_account(&cnf)?, - domains: crt.domains.to_owned(), + domains: crt.get_domains()?, algo: crt.get_algorithm()?, kp_reuse: crt.get_kp_reuse(), remote_url: crt.get_remote_url(&cnf)?, diff --git a/tacd/src/main.rs b/tacd/src/main.rs index a4225c5..ce57bd3 100644 --- a/tacd/src/main.rs +++ b/tacd/src/main.rs @@ -3,6 +3,7 @@ mod openssl_server; use crate::openssl_server::start as server_start; use acme_common::crypto::X509Certificate; use acme_common::error::Error; +use acme_common::to_idna; use clap::{App, Arg, ArgMatches}; use log::{debug, error, info}; use std::fs::File; @@ -44,6 +45,7 @@ fn init(cnf: &ArgMatches) -> Result<(), Error> { cnf.value_of("pid-file").unwrap_or(DEFAULT_PID_FILE), ); let domain = get_acme_value(cnf, "domain", "domain-file")?; + let domain = to_idna(&domain)?; let ext = get_acme_value(cnf, "acme-ext", "acme-ext-file")?; let listen_addr = cnf.value_of("listen").unwrap_or(DEFAULT_LISTEN_ADDR); let (pk, cert) = X509Certificate::from_acme_ext(&domain, &ext)?;