diff --git a/CHANGELOG.md b/CHANGELOG.md index 737edbf..b5c3c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - An account object has been added in the configuration. - Failure recovery: HTTPS requests rejected by the server that are recoverable, like the badNonce error, are now retried several times before being considered a hard failure. +- The TLS-ALPN-01 challenge is now supported. The proof is a string representation of the acmeIdentifier extension. The self-signed certificate itself has to be built by a hook. ### Changed - In the configuration, the `email` certificate field has been replaced by the `account` field which matches an account object. diff --git a/README.md b/README.md index 6d6519a..e85d7b6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The Automatic Certificate Management Environment (ACME), is an internet standard ## Key features -- HTTP-01 and DNS-01 challenges +- http-01, dns-01 and tls-alpn-01 challenges - RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates - Fully customizable challenge validation action - Fully customizable archiving method (yes, you can use git or anything else) @@ -22,8 +22,8 @@ The Automatic Certificate Management Environment (ACME), is an internet standard ## Planned features -- TLS-ALPN challenges -- daemon and certificates management via the `acmectl` tool +- A standalone server dedicated to the tls-alpn-01 challenge validation +- Daemon and certificates management via the `acmectl` tool ## Build from source diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index a5f7b2e..4f3cf95 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -19,6 +19,7 @@ pub mod structs; pub enum Challenge { Http01, Dns01, + TlsAlpn01, } impl Challenge { @@ -26,6 +27,7 @@ impl Challenge { match s.to_lowercase().as_str() { "http-01" => Ok(Challenge::Http01), "dns-01" => Ok(Challenge::Dns01), + "tls-alpn-01" => Ok(Challenge::TlsAlpn01), _ => Err(format!("{}: unknown challenge.", s).into()), } } @@ -36,6 +38,7 @@ impl fmt::Display for Challenge { let s = match self { Challenge::Http01 => "http-01", Challenge::Dns01 => "dns-01", + Challenge::TlsAlpn01 => "tls-alpn-01", }; write!(f, "{}", s) } @@ -46,6 +49,7 @@ impl PartialEq for Challenge { match (self, other) { (Challenge::Http01, structs::Challenge::Http01(_)) => true, (Challenge::Dns01, structs::Challenge::Dns01(_)) => true, + (Challenge::TlsAlpn01, structs::Challenge::TlsAlpn01(_)) => true, _ => false, } } diff --git a/acmed/src/acme_proto/structs/authorization.rs b/acmed/src/acme_proto/structs/authorization.rs index 7d47ed6..94de436 100644 --- a/acmed/src/acme_proto/structs/authorization.rs +++ b/acmed/src/acme_proto/structs/authorization.rs @@ -8,6 +8,11 @@ use serde::Deserialize; use std::fmt; use std::str::FromStr; +const ACME_OID: &str = "1.3.6.1.5.5.7.1"; +const ID_PE_ACME_ID: usize = 31; +const DER_OCTET_STRING_ID: usize = 0x04; +const DER_STRUCT_NAME: &str = "DER"; + #[derive(Deserialize)] pub struct Authorization { pub identifier: Identifier, @@ -59,7 +64,8 @@ pub enum Challenge { Http01(TokenChallenge), #[serde(rename = "dns-01")] Dns01(TokenChallenge), - // TODO: tls-alpn-01 + #[serde(rename = "tls-alpn-01")] + TlsAlpn01(TokenChallenge), #[serde(other)] Unknown, } @@ -69,7 +75,9 @@ deserialize_from_str!(Challenge); impl Challenge { pub fn get_url(&self) -> String { match self { - Challenge::Http01(tc) | Challenge::Dns01(tc) => tc.url.to_owned(), + Challenge::Http01(tc) | Challenge::Dns01(tc) | Challenge::TlsAlpn01(tc) => { + tc.url.to_owned() + } Challenge::Unknown => String::new(), } } @@ -83,6 +91,25 @@ impl Challenge { let a = b64_encode(&a); Ok(a) } + Challenge::TlsAlpn01(tc) => { + let acme_ext_name = format!("{}.{}", ACME_OID, ID_PE_ACME_ID); + let ka = tc.key_authorization(private_key)?; + let proof = sha256(ka.as_bytes()); + let proof_str = proof + .iter() + .map(|e| format!("{:02x}", e)) + .collect::>() + .join(":"); + let value = format!( + "critical,{}:{:02x}:{:02x}:{}", + DER_STRUCT_NAME, + DER_OCTET_STRING_ID, + proof.len(), + proof_str + ); + let acme_ext = format!("{}={}", acme_ext_name, value); + Ok(acme_ext) + } Challenge::Unknown => Ok(String::new()), } } @@ -90,7 +117,7 @@ impl Challenge { pub fn get_file_name(&self) -> String { match self { Challenge::Http01(tc) => tc.token.to_owned(), - Challenge::Dns01(_) => String::new(), + Challenge::Dns01(_) | Challenge::TlsAlpn01(_) => String::new(), Challenge::Unknown => String::new(), } }