use std::borrow::Cow;
use std::io::{self, Write};
use chrono::prelude::*;
use super::{
hash, Algorithm, BodyCanonicalisation, Canonicalisation, Error,
HashAlgorithm, Header, HeaderCanonicalisation, SignatureAlgorithm,
};
pub type KeyPair = openssl::pkey::PKey<openssl::pkey::Private>;
pub struct Signer<'a> {
subs: Vec<SubSigner<'a>>,
}
struct SubSigner<'a> {
header: Header<'a>,
hasher: hash::BodyHasher,
key: &'a KeyPair,
}
impl<'a> Signer<'a> {
pub fn new(keys: &'a [(String, KeyPair)], template: &Header<'a>) -> Self {
Self {
subs: keys
.iter()
.map(|&(ref selector, ref key)| {
let mut header = template.to_owned();
header.raw = None;
header.body_hash.clear();
header.signature.clear();
header.selector = Cow::Borrowed(selector);
header.algorithm.signature = match key.id() {
openssl::pkey::Id::RSA => SignatureAlgorithm::Rsa,
openssl::pkey::Id::ED25519 => {
SignatureAlgorithm::Ed25519
},
id => panic!("unexpected key type: {id:?}"),
};
let hasher = hash::BodyHasher::new(&header);
SubSigner {
header,
hasher,
key,
}
})
.collect(),
}
}
pub fn default_template(
now: DateTime<Utc>,
sdid: Cow<'_, str>,
) -> Header<'_> {
Header {
raw: None,
version: 1,
algorithm: Algorithm {
hash: HashAlgorithm::Sha256,
signature: SignatureAlgorithm::Ed25519,
},
signature: Vec::new(),
body_hash: Vec::new(),
canonicalisation: Canonicalisation {
header: HeaderCanonicalisation::Relaxed,
body: BodyCanonicalisation::Simple,
},
sdid,
signed_headers: vec![
Cow::Borrowed("CC"),
Cow::Borrowed("Content-Type"),
Cow::Borrowed("Date"),
Cow::Borrowed("From"),
Cow::Borrowed("In-Reply-To"),
Cow::Borrowed("Message-ID"),
Cow::Borrowed("References"),
Cow::Borrowed("Reply-To"),
Cow::Borrowed("Subject"),
Cow::Borrowed("To"),
],
auid: None,
body_length: None,
dns_txt: true,
selector: Cow::Borrowed(""),
signature_timestamp: Some(now),
signature_expiration: Some(now + chrono::Duration::days(7)),
}
}
pub fn finish(self, header_block: &[u8]) -> String {
let mut prepend = String::new();
for sub in self.subs {
match sub.finish(header_block) {
Ok(header) => {
prepend.push_str(&header);
prepend.push_str("\r\n");
},
#[cfg(test)]
Err(e) => {
panic!("DKIM signature failed: {e}");
},
#[cfg(not(test))]
Err(e) => {
log::error!("DKIM signature failed: {e}");
},
}
}
prepend
}
}
impl Write for Signer<'_> {
fn write(&mut self, src: &[u8]) -> io::Result<usize> {
for sub in &mut self.subs {
sub.hasher.write_all(src)?;
}
Ok(src.len())
}
fn flush(&mut self) -> io::Result<()> {
for sub in &mut self.subs {
sub.hasher.flush()?;
}
Ok(())
}
}
impl SubSigner<'_> {
fn finish(mut self, header_block: &[u8]) -> Result<String, Error> {
self.header.body_hash = self.hasher.finish(&self.header)?;
let mut hash_data = hash::header_hash_data(&self.header, header_block);
let mut signer = match self.header.algorithm.signature {
SignatureAlgorithm::Rsa => openssl::sign::Signer::new(
self.header.algorithm.hash.message_digest(),
self.key,
),
SignatureAlgorithm::Ed25519 => {
let mut hasher = openssl::hash::Hasher::new(
self.header.algorithm.hash.message_digest(),
)
.map_err(Error::Ssl)?;
hasher.update(&hash_data).map_err(Error::Ssl)?;
let bytes = hasher.finish().map_err(Error::Ssl)?;
hash_data.clear();
hash_data.extend_from_slice(&bytes);
openssl::sign::Signer::new_without_digest(self.key)
},
}
.map_err(Error::Ssl)?;
self.header.signature =
signer.sign_oneshot_to_vec(&hash_data).map_err(Error::Ssl)?;
Ok(self.header.raw().into_owned().text.into_owned())
}
}
#[cfg(test)]
mod test {
use super::super::{
split_message, Outcome, TxtRecordEntry, VerificationEnvironment,
Verifier,
};
use super::*;
use crate::{support::dns, test_data::*};
#[test]
fn test_sign_and_verify() {
let (original_headers, body) = split_message(CHRISTMAS_TREE);
let rsa_pair = openssl::rsa::Rsa::generate(1024).unwrap();
let keys = [
(
"ed".to_owned(),
openssl::pkey::PKey::generate_ed25519().unwrap(),
),
(
"rsa".to_owned(),
openssl::pkey::PKey::from_rsa(rsa_pair).unwrap(),
),
];
let mut signer = Signer::new(
&keys,
&Signer::default_template(Utc::now(), Cow::Borrowed("example.com")),
);
signer.write_all(body).unwrap();
let signed_headers = signer.finish(original_headers);
let ver_env = VerificationEnvironment {
now: Utc::now(),
txt_records: vec![
TxtRecordEntry {
sdid: "example.com".to_owned(),
selector: "ed".to_owned(),
txt: Ok(format!(
"k=ed25519;p={}",
base64::encode(&keys[0].1.raw_public_key().unwrap()),
)
.into()),
},
TxtRecordEntry {
sdid: "example.com".to_owned(),
selector: "rsa".to_owned(),
txt: Ok(format!(
"k=rsa;p={}",
base64::encode(&keys[1].1.public_key_to_der().unwrap()),
)
.into()),
},
],
};
let mut combined_headers = signed_headers.as_bytes().to_vec();
combined_headers.extend_from_slice(&original_headers);
let mut verifier = Verifier::new(&combined_headers);
verifier.write_all(body).unwrap();
let results = verifier.finish(&ver_env).collect::<Vec<_>>();
assert_eq!(
vec![
Outcome {
sdid: Some(dns::Name::from_ascii("example.com").unwrap()),
selector: Some("ed".to_owned()),
error: None,
},
Outcome {
sdid: Some(dns::Name::from_ascii("example.com").unwrap()),
selector: Some("rsa".to_owned()),
error: None,
},
],
results,
);
}
}