Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 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 Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cfdkim"
version = "0.2.6"
version = "0.3.0"
authors = ["Sven Sauleau <sven@cloudflare.com>"]
edition = "2021"
description = "DKIM (RFC6376) implementation"
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ let private_key =
rsa::RsaPrivateKey::read_pkcs1_pem_file(Path::new("./test/keys/2022.private"))?;

let signer = SignerBuilder::new()
.with_signed_headers(&["From", "Subject"])?
.with_signed_headers(["From", "Subject"])?
.with_private_key(private_key)
.with_selector("2020")
.with_logger(&logger)
.with_signing_domain("example.com")
.build()?;
let signature = signer.sign(&email)?;
Expand Down
14 changes: 8 additions & 6 deletions src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fn select_headers<'a>(
}

pub(crate) fn compute_headers_hash<'a, 'b>(
logger: &slog::Logger,
logger: Option<&slog::Logger>,
canonicalization_type: canonicalization::Type,
headers: &'b str,
hash_algo: HashAlgo,
Expand Down Expand Up @@ -139,7 +139,9 @@ pub(crate) fn compute_headers_hash<'a, 'b>(

input.extend_from_slice(&canonicalized_value);
}
debug!(logger, "headers to hash: {:?}", input);
if let Some(logger) = logger {
debug!(logger, "headers to hash: {:?}", input);
}

let hash = match hash_algo {
HashAlgo::RsaSha1 => hash_sha1(&input),
Expand Down Expand Up @@ -323,7 +325,7 @@ Hello Alice
let logger = slog::Logger::root(slog::Discard, slog::o!());
assert_eq!(
compute_headers_hash(
&logger,
Some(&logger),
canonicalization_type.clone(),
&headers,
hash_algo,
Expand All @@ -339,7 +341,7 @@ Hello Alice
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_headers_hash(
&logger,
Some(&logger),
canonicalization_type,
&headers,
hash_algo,
Expand Down Expand Up @@ -373,7 +375,7 @@ Hello Alice
let logger = slog::Logger::root(slog::Discard, slog::o!());
assert_eq!(
compute_headers_hash(
&logger,
Some(&logger),
canonicalization_type.clone(),
&headers,
hash_algo,
Expand All @@ -389,7 +391,7 @@ Hello Alice
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_headers_hash(
&logger,
Some(&logger),
canonicalization_type,
&headers,
hash_algo,
Expand Down
9 changes: 6 additions & 3 deletions src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ impl DKIMHeaderBuilder {
self
}

pub(crate) fn set_signed_headers(self, headers: &[&str]) -> Self {
let headers: Vec<String> = headers.iter().map(|h| h.to_lowercase()).collect();
pub(crate) fn set_signed_headers(self, headers: &Vec<String>) -> Self {
let value = headers.join(":");
self.add_tag("h", &value)
}
Expand Down Expand Up @@ -106,11 +105,15 @@ mod tests {
assert_eq!(header.raw_bytes, "v=1; a=something;".to_owned());
}

fn signed_header_list(headers: &[&str]) -> Vec<String> {
headers.into_iter().map(|h| h.to_lowercase()).collect()
}

#[test]
fn test_dkim_header_builder_signed_headers() {
let header = DKIMHeaderBuilder::new()
.add_tag("v", "2")
.set_signed_headers(&["header1", "header2", "header3"])
.set_signed_headers(&signed_header_list(&["header1", "header2", "header3"]))
.build()
.unwrap();
assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ async fn verify_email_header<'a>(
email,
)?;
let computed_headers_hash = hash::compute_headers_hash(
logger,
Some(logger),
header_canonicalization_type.clone(),
&dkim_header.get_required_tag("h"),
hash_algo.clone(),
Expand Down
4 changes: 1 addition & 3 deletions src/roundtrip_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@ mod tests {

let private_key =
rsa::RsaPrivateKey::read_pkcs1_pem_file(Path::new("./test/keys/2022.private")).unwrap();
let logger = test_logger();
let time = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap();

let signer = SignerBuilder::new()
.with_signed_headers(&["From", "Subject"])
.with_signed_headers(["From", "Subject"])
.unwrap()
.with_private_key(DkimPrivateKey::Rsa(private_key))
.with_selector("2022")
.with_logger(&logger)
.with_signing_domain(domain)
.with_time(time)
.build()
Expand Down
79 changes: 34 additions & 45 deletions src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,24 @@ use crate::header::DKIMHeaderBuilder;
use crate::{canonicalization, hash, DKIMError, DkimPrivateKey, HEADER};

/// Builder for the Signer
pub struct SignerBuilder<'a> {
signed_headers: Option<&'a [&'a str]>,
pub struct SignerBuilder {
signed_headers: Option<Vec<String>>,
private_key: Option<DkimPrivateKey>,
selector: Option<&'a str>,
signing_domain: Option<&'a str>,
selector: Option<String>,
signing_domain: Option<String>,
time: Option<chrono::DateTime<chrono::offset::Utc>>,
header_canonicalization: canonicalization::Type,
body_canonicalization: canonicalization::Type,
logger: Option<&'a slog::Logger>,
expiry: Option<chrono::Duration>,
}

impl<'a> SignerBuilder<'a> {
impl SignerBuilder {
/// New builder
pub fn new() -> Self {
Self {
signed_headers: None,
private_key: None,
selector: None,
logger: None,
signing_domain: None,
expiry: None,
time: None,
Expand All @@ -40,9 +38,16 @@ impl<'a> SignerBuilder<'a> {

/// Specify headers to be used in the DKIM signature
/// The From: header is required.
pub fn with_signed_headers(mut self, headers: &'a [&'a str]) -> Result<Self, DKIMError> {
let from = headers.iter().find(|h| h.to_lowercase() == "from");
if from.is_none() {
pub fn with_signed_headers(
mut self,
headers: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, DKIMError> {
let headers: Vec<String> = headers
.into_iter()
.map(|h| h.into().to_lowercase())
.collect();

if !headers.iter().any(|h| h.eq_ignore_ascii_case("from")) {
return Err(DKIMError::BuilderError("missing From in signed headers"));
}

Expand All @@ -57,14 +62,14 @@ impl<'a> SignerBuilder<'a> {
}

/// Specify the private key used to sign the email
pub fn with_selector(mut self, value: &'a str) -> Self {
self.selector = Some(value);
pub fn with_selector(mut self, value: impl Into<String>) -> Self {
self.selector = Some(value.into());
self
}

/// Specify for which domain the email should be signed for
pub fn with_signing_domain(mut self, value: &'a str) -> Self {
self.signing_domain = Some(value);
pub fn with_signing_domain(mut self, value: impl Into<String>) -> Self {
self.signing_domain = Some(value.into());
self
}

Expand All @@ -80,12 +85,6 @@ impl<'a> SignerBuilder<'a> {
self
}

/// Specify a logger
pub fn with_logger(mut self, logger: &'a slog::Logger) -> Self {
self.logger = Some(logger);
self
}

/// Specify current time. Mostly used for testing
pub fn with_time(mut self, value: chrono::DateTime<chrono::offset::Utc>) -> Self {
self.time = Some(value);
Expand All @@ -99,9 +98,9 @@ impl<'a> SignerBuilder<'a> {
}

/// Build an instance of the Signer
/// Must be provided: signed_headers, private_key, selector, logger and
/// Must be provided: signed_headers, private_key, selector and
/// signing_domain.
pub fn build(self) -> Result<Signer<'a>, DKIMError> {
pub fn build(self) -> Result<Signer, DKIMError> {
use DKIMError::BuilderError;

let private_key = self
Expand All @@ -120,10 +119,9 @@ impl<'a> SignerBuilder<'a> {
selector: self
.selector
.ok_or(BuilderError("missing required selector"))?,
logger: self.logger.ok_or(BuilderError("missing required logger"))?,
signing_domain: self
.signing_domain
.ok_or(BuilderError("missing required logger"))?,
.ok_or(BuilderError("missing required signing domain"))?,
header_canonicalization: self.header_canonicalization,
body_canonicalization: self.body_canonicalization,
expiry: self.expiry,
Expand All @@ -133,27 +131,26 @@ impl<'a> SignerBuilder<'a> {
}
}

impl<'a> Default for SignerBuilder<'a> {
impl Default for SignerBuilder {
fn default() -> Self {
Self::new()
}
}

pub struct Signer<'a> {
signed_headers: &'a [&'a str],
pub struct Signer {
signed_headers: Vec<String>,
private_key: DkimPrivateKey,
selector: &'a str,
signing_domain: &'a str,
selector: String,
signing_domain: String,
header_canonicalization: canonicalization::Type,
body_canonicalization: canonicalization::Type,
logger: &'a slog::Logger,
expiry: Option<chrono::Duration>,
hash_algo: hash::HashAlgo,
time: Option<chrono::DateTime<chrono::offset::Utc>>,
}

/// DKIM signer. Use the [SignerBuilder] to build an instance.
impl<'a> Signer<'a> {
impl Signer {
/// Sign a message
/// As specified in <https://datatracker.ietf.org/doc/html/rfc6376#section-5>
pub fn sign<'b>(&self, email: &'b mailparse::ParsedMail<'b>) -> Result<String, DKIMError> {
Expand Down Expand Up @@ -203,8 +200,8 @@ impl<'a> Signer<'a> {
let mut builder = DKIMHeaderBuilder::new()
.add_tag("v", "1")
.add_tag("a", hash_algo)
.add_tag("d", self.signing_domain)
.add_tag("s", self.selector)
.add_tag("d", &self.signing_domain)
.add_tag("s", &self.selector)
.add_tag(
"c",
&format!(
Expand All @@ -214,7 +211,7 @@ impl<'a> Signer<'a> {
),
)
.add_tag("bh", body_hash)
.set_signed_headers(self.signed_headers);
.set_signed_headers(&self.signed_headers);
if let Some(expiry) = self.expiry {
builder = builder.set_expiry(expiry)?;
}
Expand Down Expand Up @@ -248,7 +245,7 @@ impl<'a> Signer<'a> {
let signed_headers = dkim_header.get_required_tag("h");

hash::compute_headers_hash(
self.logger,
None,
canonicalization,
&signed_headers,
self.hash_algo.clone(),
Expand All @@ -265,10 +262,6 @@ mod tests {
use rsa::pkcs1::DecodeRsaPrivateKey;
use std::{fs, path::Path};

fn test_logger() -> slog::Logger {
slog::Logger::root(slog::Discard, slog::o!())
}

#[test]
fn test_sign_rsa() {
let email = mailparse::parse_mail(
Expand All @@ -283,15 +276,13 @@ Hello Alice

let private_key =
rsa::RsaPrivateKey::read_pkcs1_pem_file(Path::new("./test/keys/2022.private")).unwrap();
let logger = test_logger();
let time = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 1).unwrap();

let signer = SignerBuilder::new()
.with_signed_headers(&["From", "Subject"])
.with_signed_headers(["From", "Subject"])
.unwrap()
.with_private_key(DkimPrivateKey::Rsa(private_key))
.with_selector("s20")
.with_logger(&logger)
.with_signing_domain("example.com")
.with_time(time)
.build()
Expand Down Expand Up @@ -330,13 +321,12 @@ Joe."#
secret: secret_key,
};

let logger = test_logger();
let time = chrono::Utc
.with_ymd_and_hms(2018, 6, 10, 13, 38, 29)
.unwrap();

let signer = SignerBuilder::new()
.with_signed_headers(&[
.with_signed_headers([
"From",
"To",
"Subject",
Expand All @@ -351,7 +341,6 @@ Joe."#
.with_body_canonicalization(canonicalization::Type::Relaxed)
.with_header_canonicalization(canonicalization::Type::Relaxed)
.with_selector("brisbane")
.with_logger(&logger)
.with_signing_domain("football.example.com")
.with_time(time)
.build()
Expand Down