Skip to content

Commit

Permalink
message: switch ed25519 dkim signing to cfdkim
Browse files Browse the repository at this point in the history
With the upgraded ed25519-dalek crate, it's now possible to
pass in either DER or PEM encoded PKCS8 signing keys, which
makes it feasible to remove the mail-auth dep from this crate.

That in turns reduces the amount of code in here, which is nice.
  • Loading branch information
wez committed Aug 23, 2023
1 parent 4ad227b commit 05e52d4
Show file tree
Hide file tree
Showing 7 changed files with 39 additions and 88 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/dkim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ default = ["openssl"]
[dependencies]
base64 = "0.21.0"
chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] }
ed25519-dalek = {workspace=true}
ed25519-dalek = {workspace=true, features=["pkcs8"]}
futures = "0.3.28"
indexmap = "1.9.3"
mailparse = "0.14"
Expand Down
24 changes: 23 additions & 1 deletion crates/dkim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::hash::HeaderList;
use base64::engine::general_purpose;
use base64::Engine;
use ed25519_dalek::SigningKey;
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
Expand Down Expand Up @@ -47,7 +48,7 @@ pub(crate) enum DkimPublicKey {
#[derive(Debug)]
pub enum DkimPrivateKey {
Rsa(RsaPrivateKey),
Ed25519(ed25519_dalek::SigningKey),
Ed25519(SigningKey),
#[cfg(feature = "openssl")]
OpenSSLRsa(openssl::rsa::Rsa<openssl::pkey::Private>),
}
Expand Down Expand Up @@ -106,6 +107,27 @@ impl DkimPrivateKey {
})?;
Self::rsa_key(&data)
}

/// Parse PKCS8 encoded ed25519 key data into a DkimPrivateKey.
/// Both DER and PEM are supported
pub fn ed25519_key(data: &[u8]) -> Result<Self, DKIMError> {
let mut errors = vec![];

match SigningKey::from_pkcs8_der(data) {
Ok(key) => return Ok(Self::Ed25519(key)),
Err(err) => errors.push(format!("Ed25519 SigningKey::from_pkcs8_der: {err:#}")),
}

match std::str::from_utf8(data) {
Ok(s) => match SigningKey::from_pkcs8_pem(s) {
Ok(key) => return Ok(Self::Ed25519(key)),
Err(err) => errors.push(format!("Ed25519 SigningKey::from_pkcs8_pem: {err:#}")),
},
Err(err) => errors.push(format!("ed25519_key: data is not UTF-8: {err:#}")),
}

Err(DKIMError::PrivateKeyLoadError(errors.join(". ")))
}
}

// https://datatracker.ietf.org/doc/html/rfc6376#section-6.1.3 Step 4
Expand Down
1 change: 0 additions & 1 deletion crates/message/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ futures = "0.3"
kumo-log-types = {path="../kumo-log-types"}
lazy_static = "1.4"
lruttl = {path="../lruttl"}
mail-auth = "0.3"
mailparse = "0.14"
mail-parser = "0.8"
mlua = {workspace=true, features=["vendored", "lua54", "async", "send", "serialize"]}
Expand Down
93 changes: 9 additions & 84 deletions crates/message/src/dkim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ use cfdkim::DkimPrivateKey;
use config::{from_lua_value, get_or_create_sub_module};
use data_loader::KeySource;
use lruttl::LruCacheWithTtl;
use mail_auth::common::crypto::{Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey};
use mail_auth::common::headers::HeaderWriter;
use mail_auth::dkim::{Canonicalization, DkimSigner, Done, NeedDomain};
use mlua::prelude::LuaUserData;
use mlua::{Lua, Value};
use serde::Deserialize;
use std::sync::Arc;
use std::time::{Duration, Instant};

lazy_static::lazy_static! {
static ref SIGNER_CACHE: LruCacheWithTtl<SignerConfig, Arc<SignerInner>> = LruCacheWithTtl::new(1024);
static ref SIGNER_CACHE: LruCacheWithTtl<SignerConfig, Arc<CFSigner>> = LruCacheWithTtl::new(1024);
}

#[derive(Deserialize, Hash, Eq, PartialEq, Copy, Clone)]
Expand All @@ -28,30 +25,12 @@ impl Default for Canon {
}
}

impl Into<Canonicalization> for Canon {
fn into(self) -> Canonicalization {
match self {
Self::Relaxed => Canonicalization::Relaxed,
Self::Simple => Canonicalization::Simple,
}
}
}

#[derive(Deserialize, Hash, Eq, PartialEq, Copy, Clone)]
pub enum HashAlgo {
Sha1,
Sha256,
}

impl Into<HashAlgorithm> for HashAlgo {
fn into(self) -> HashAlgorithm {
match self {
Self::Sha1 => HashAlgorithm::Sha1,
Self::Sha256 => HashAlgorithm::Sha256,
}
}
}

#[derive(Deserialize, Hash, PartialEq, Eq, Clone)]
pub struct SignerConfig {
domain: String,
Expand Down Expand Up @@ -85,34 +64,6 @@ impl SignerConfig {
300
}

fn configure_signer<T: SigningKey>(
&self,
signer: DkimSigner<T, NeedDomain>,
) -> DkimSigner<T, Done> {
let mut signer = signer
.domain(self.domain.clone())
.selector(self.selector.clone())
.headers(self.headers.clone());
if let Some(atps) = &self.atps {
signer = signer.atps(atps.clone());
}
if let Some(atpsh) = self.atpsh {
signer = signer.atpsh(atpsh.into());
}
if let Some(agent_user_identifier) = &self.agent_user_identifier {
signer = signer.agent_user_identifier(agent_user_identifier);
}
if let Some(expiration) = self.expiration {
signer = signer.expiration(expiration);
}
signer = signer.body_length(self.body_length);
signer = signer.reporting(self.reporting);
signer = signer.header_canonicalization(self.header_canonicalization.into());
signer = signer.body_canonicalization(self.body_canonicalization.into());

signer
}

fn configure_cfdkim(&self, key: DkimPrivateKey) -> anyhow::Result<cfdkim::Signer> {
if self.atps.is_some() {
anyhow::bail!("atps is not currently supported for RSA keys");
Expand Down Expand Up @@ -152,35 +103,15 @@ impl SignerConfig {
}
}

pub enum SignerInner {
RsaSha256(DkimSigner<RsaKey<Sha256>, Done>),
Ed25519(DkimSigner<Ed25519Key, Done>),
CFDKIM(CFSigner),
}

#[derive(Clone)]
pub struct Signer(Arc<SignerInner>);
pub struct Signer(Arc<CFSigner>);

impl Signer {
pub fn sign(&self, message: &[u8]) -> anyhow::Result<String> {
self.0.sign(message)
}
}

impl SignerInner {
fn sign(&self, message: &[u8]) -> anyhow::Result<String> {
let sig = match self {
Self::RsaSha256(signer) => signer.sign(message),
Self::Ed25519(signer) => signer.sign(message),
Self::CFDKIM(signer) => return signer.sign(message),
}
.map_err(|err| anyhow::anyhow!("{err:#}"))?;

let header = sig.to_header();
Ok(header)
}
}

impl LuaUserData for Signer {}

pub fn register<'lua>(lua: &'lua Lua) -> anyhow::Result<()> {
Expand All @@ -207,7 +138,7 @@ pub fn register<'lua>(lua: &'lua Lua) -> anyhow::Result<()> {
.configure_cfdkim(key)
.map_err(|err| mlua::Error::external(format!("{err:#}")))?;

let inner = Arc::new(SignerInner::CFDKIM(CFSigner { signer }));
let inner = Arc::new(CFSigner { signer });

let expiration = Instant::now() + Duration::from_secs(params.ttl);
SIGNER_CACHE.insert(params, Arc::clone(&inner), expiration);
Expand All @@ -231,21 +162,15 @@ pub fn register<'lua>(lua: &'lua Lua) -> anyhow::Result<()> {
.await
.map_err(|err| mlua::Error::external(format!("{:?}: {err:#}", params.key)))?;

let mut errors = vec![];
let key = DkimPrivateKey::ed25519_key(&data)
.map_err(|err| mlua::Error::external(format!("{:?}: {err}", params.key)))?;

let key = Ed25519Key::from_pkcs8_der(&data)
.or_else(|err| {
errors.push(format!("from_pkcs8_der: {err:#}"));
Ed25519Key::from_pkcs8_maybe_unchecked_der(&data)
})
.map_err(|err| {
errors.push(format!("from_pkcs8_maybe_unchecked_der: {err:#}"));
mlua::Error::external(format!("{:?}: {}", params.key, errors.join(", ")))
})?;
let signer = params
.configure_cfdkim(key)
.map_err(|err| mlua::Error::external(format!("{err:#}")))?;

let signer = params.configure_signer(DkimSigner::from_key(key));
let inner = Arc::new(CFSigner { signer });

let inner = Arc::new(SignerInner::Ed25519(signer));
let expiration = Instant::now() + Duration::from_secs(params.ttl);
SIGNER_CACHE.insert(params, Arc::clone(&inner), expiration);

Expand Down
2 changes: 2 additions & 0 deletions docs/changelog/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
base32, base64 and hex strings.
* [kumo.dns.configure_resolver](../reference/kumo.dns/configure_resolver.md) for
adjusting DNS resolver configuration.
* [kumo.dkim.ed25519_signer](../reference/kumo.dkim/ed25519_signer.md) now also
supports loading signing keys that are in PEM encoded PKCS8 format.

## Fixes
* Loading secrets from HashiCorp Vault failed to parse underlying json data into
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/kumo.dkim/ed25519_signer.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ which must contain the matching public and private key pair.
If the data cannot be loaded as V2, then it will fall back
to try to load V1 data, which contains just the private key.

{{since('dev', indent=True)}}
We now support loading either DER or PEM encoded PKCS8
private keys.

```lua
-- Called once the body has been received.
-- For multi-recipient mail, this is called for each recipient.
Expand Down

0 comments on commit 05e52d4

Please sign in to comment.