From 32830cbd048098428764308093dffbf03c9df32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20Br=C3=A9ard?= Date: Sun, 13 Jun 2021 15:10:20 +0200 Subject: [PATCH] Add support for Ed25519 and Ed448 account keys and certificates Fixes #36 --- CHANGELOG.md | 1 + README.md | 3 +- acme_common/build.rs | 4 ++ acme_common/src/crypto/openssl_keys.rs | 37 +++++++++++++++--- acme_common/src/tests/crypto_keys.rs | 52 +++++++++++++++++++++++++- man/en/acmed.toml.5 | 8 ++++ 6 files changed, 96 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c96c6..d3005e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Add support for Ed25519 and Ed448 account keys and certificates. - In addition to `restart`, the Polkit rule also allows the `reload`, `try-restart`, `reload-or-restart` and `try-reload-or-restart` verbs. diff --git a/README.md b/README.md index cf95ca8..e31b550 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The Automatic Certificate Management Environment (ACME), is an internet standard - 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, ECDSA P-384 and ECDSA P-521 certificates +- RSA 2048, RSA 4096, ECDSA P-256, ECDSA P-384, ECDSA P-521, Ed25519 and Ed448 certificates and account keys - Internationalized domain names support - Fully customizable challenge validation action - Fully customizable archiving method (yes, you can use git or anything else) @@ -38,7 +38,6 @@ The Automatic Certificate Management Environment (ACME), is an internet standard ## Planned features - STAR certificates [RFC 8739](https://tools.ietf.org/html/rfc8739) -- EdDSA support: Ed25519 and Ed448 account keys and certificates - Daemon and certificates management via the `acmectl` tool - HTTP/2 support diff --git a/acme_common/build.rs b/acme_common/build.rs index cbe5839..beef778 100644 --- a/acme_common/build.rs +++ b/acme_common/build.rs @@ -8,9 +8,13 @@ macro_rules! set_rustc_env_var { fn main() { if env::var("DEP_OPENSSL_VERSION_NUMBER").is_ok() { + println!("cargo:rustc-cfg=ed25519"); + println!("cargo:rustc-cfg=ed448"); set_rustc_env_var!("ACMED_TLS_LIB_NAME", "OpenSSL"); } if env::var("DEP_OPENSSL_LIBRESSL_VERSION_NUMBER").is_ok() { + println!("cargo:rustc-cfg=ed25519"); + println!("cargo:rustc-cfg=ed448"); set_rustc_env_var!("ACMED_TLS_LIB_NAME", "LibreSSL"); } } diff --git a/acme_common/src/crypto/openssl_keys.rs b/acme_common/src/crypto/openssl_keys.rs index 5f97f12..8f4a142 100644 --- a/acme_common/src/crypto/openssl_keys.rs +++ b/acme_common/src/crypto/openssl_keys.rs @@ -247,12 +247,40 @@ impl KeyPair { return Err("not an EdDSA elliptic curve".into()); } }; - let x = ""; + + /* + * /!\ WARNING: HAZARDOUS AND UGLY CODE /!\ + * + * I couldn't find a way to get the value of `x` using the OpenSSL + * interface, therefore I had to hack my way arround. + * + * The idea behind this hack is to export the public key in PEM, then + * get the PEM base64 part, convert it to base64url without padding + * and finally truncate the first part so only the value of `x` + * remains. + */ + + // -----BEGIN UGLY----- + let mut x = String::new(); + let public_pem = self.public_key_to_pem()?; + let public_pem = String::from_utf8(public_pem)?; + for pem_line in public_pem.lines() { + if !pem_line.is_empty() && !pem_line.starts_with("-----") { + x += &pem_line + .trim() + .trim_end_matches('=') + .replace("/", "_") + .replace("+", "-"); + } + } + x.replace_range(..16, ""); + // -----END UGLY----- + let jwk = if thumbprint { json!({ "crv": crv, "kty": "OKP", - "x": x, + "x": &x, }) } else { json!({ @@ -260,11 +288,10 @@ impl KeyPair { "crv": crv, "kty": "OKP", "use": "sig", - "x": x, + "x": &x, }) }; - //Ok(jwk) - Err("TODO: implement get_eddsa_jwk (require binding to EVP_PKEY_get1_ED25519, which requires OpenSSL 3.0)".into()) + Ok(jwk) } } diff --git a/acme_common/src/tests/crypto_keys.rs b/acme_common/src/tests/crypto_keys.rs index 1a71c61..8019410 100644 --- a/acme_common/src/tests/crypto_keys.rs +++ b/acme_common/src/tests/crypto_keys.rs @@ -92,6 +92,10 @@ PO2/53dpt/yV5zOPrYPEoKs4t973nbt46IUN19lLF/s= const KEY_ECDSA_ED25519_PEM: &str = r#"-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIJhpRNsiUzoWqNkpJKCtKV5++Tttz3locu1gQKkQnrOa -----END PRIVATE KEY-----"#; +#[cfg(ed25519)] +const KEY_ECDSA_ED25519_PEM_BIS: &str = r#"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKa3WD0qeUToPQKSwa9cTsLPgCovqAtXMhlMX2KYBz0o +-----END PRIVATE KEY-----"#; #[cfg(ed448)] const KEY_ECDSA_ED448_PEM: &str = r#"-----BEGIN PRIVATE KEY----- MEcCAQAwBQYDK2VxBDsEOcFBwsH4zU7u5RgFh48MgJPzXyjN5uXxDapZv4rG6opU @@ -318,6 +322,50 @@ fn test_ed25519_jwk_thumbprint() { ); } +#[cfg(ed25519)] +#[test] +fn test_ed25519_jwk_bis() { + let k = KeyPair::from_pem(KEY_ECDSA_ED25519_PEM_BIS.as_bytes()).unwrap(); + let jwk = k.jwk_public_key().unwrap(); + assert!(jwk.is_object()); + let jwk = jwk.as_object().unwrap(); + assert_eq!(jwk.len(), 5); + assert!(jwk.contains_key("kty")); + assert!(jwk.contains_key("crv")); + assert!(jwk.contains_key("x")); + assert!(jwk.contains_key("use")); + assert!(jwk.contains_key("alg")); + assert_eq!(jwk.get("kty").unwrap(), "OKP"); + assert_eq!(jwk.get("crv").unwrap(), "Ed25519"); + assert_eq!( + jwk.get("x").unwrap(), + "i9K0eV5qOJ_l_TWjWFLm8R-JbyGdlqFFeL_J0eEXFnc" + ); + assert_eq!(jwk.get("use").unwrap(), "sig"); + assert_eq!(jwk.get("alg").unwrap(), "EdDSA"); +} + +#[cfg(ed25519)] +#[test] +fn test_ed25519_jwk_thumbprint_bis() { + let k = KeyPair::from_pem(KEY_ECDSA_ED25519_PEM_BIS.as_bytes()).unwrap(); + let jwk = k.jwk_public_key_thumbprint().unwrap(); + assert!(jwk.is_object()); + let jwk = jwk.as_object().unwrap(); + assert_eq!(jwk.len(), 3); + assert!(jwk.contains_key("kty")); + assert!(jwk.contains_key("crv")); + assert!(jwk.contains_key("x")); + assert!(!jwk.contains_key("use")); + assert!(!jwk.contains_key("alg")); + assert_eq!(jwk.get("kty").unwrap(), "OKP"); + assert_eq!(jwk.get("crv").unwrap(), "Ed25519"); + assert_eq!( + jwk.get("x").unwrap(), + "i9K0eV5qOJ_l_TWjWFLm8R-JbyGdlqFFeL_J0eEXFnc" + ); +} + #[cfg(ed448)] #[test] fn test_ed448_jwk() { @@ -335,7 +383,7 @@ fn test_ed448_jwk() { assert_eq!(jwk.get("crv").unwrap(), "Ed448"); assert_eq!( jwk.get("x").unwrap(), - "ib9GZ8b1hip3UMzkkNBdMF4JWBTZojxsNHK-jQBH94SY3boVs4Oeo291E1dGXz7RUMqIXjkSbU4EA" + "b9GZ8b1hip3UMzkkNBdMF4JWBTZojxsNHK-jQBH94SY3boVs4Oeo291E1dGXz7RUMqIXjkSbU4EA" ); assert_eq!(jwk.get("use").unwrap(), "sig"); assert_eq!(jwk.get("alg").unwrap(), "EdDSA"); @@ -358,6 +406,6 @@ fn test_ed448_jwk_thumbprint() { assert_eq!(jwk.get("crv").unwrap(), "Ed448"); assert_eq!( jwk.get("x").unwrap(), - "ib9GZ8b1hip3UMzkkNBdMF4JWBTZojxsNHK-jQBH94SY3boVs4Oeo291E1dGXz7RUMqIXjkSbU4EA" + "b9GZ8b1hip3UMzkkNBdMF4JWBTZojxsNHK-jQBH94SY3boVs4Oeo291E1dGXz7RUMqIXjkSbU4EA" ); } diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index c3589f8..9373905 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -64,6 +64,10 @@ Names of hooks that will be called during operations on the account storage file Name of the asymmetric cryptography algorithm used to generate the key pair. Possible values are: .Bl -dash -compact .It +ed25519 +.It +ed448 +.It ecdsa_p256 .Aq default .It @@ -176,6 +180,10 @@ The IP address. Name of the asymmetric cryptography algorithm used to generate the certificate's key pair. Possible values are: .Bl -dash -compact .It +ed25519 +.It +ed448 +.It ecdsa_p256 .It ecdsa_p384