Sign the message

This commit is contained in:
Rodolphe Bréard 2023-04-15 19:24:04 +02:00
parent 39a2186b61
commit 08297ac5e0
7 changed files with 220 additions and 17 deletions

View file

@ -17,7 +17,8 @@ futures = { version = "0.3.28", default-features = false, features = ["std"] }
log = { version = "0.4.17", default-features = false }
nom = { version = "7.1.3", default-features = false }
rand = { version = "0.8.5", default-features = false, features = ["std"] }
rsa = { version = "0.8.2", default-features = false, features = ["std"] }
rsa = { version = "0.8.2", default-features = false, features = ["std", "sha2"] }
sha2 = { version = "0.10.6", default-features = false, features = ["asm", "std"] }
sqlx = { version = "0.6.3", default-features = false, features = ["runtime-tokio-native-tls", "macros", "migrate", "sqlite", "time"] }
tokio = { version = "1.27.0", default-features = false, features = ["rt-multi-thread", "io-std", "io-util", "macros", "sync", "time", "process"] }
uuid = { version = "1.3.1", default-features = false, features = ["v4", "fast-rng"] }

View file

@ -11,7 +11,7 @@ use tokio::time::sleep;
pub enum Action<'a> {
ReadLine(Arc<RwLock<StdinReader>>),
RotateKeys((&'a SqlitePool, &'a Config)),
SendMessage((Message, &'a Config)),
SendMessage((&'a SqlitePool, &'a Config, Message)),
}
pub enum ActionResult {
@ -36,8 +36,8 @@ pub async fn new_action(action: Action<'_>) -> ActionResult {
sleep(duration).await;
ActionResult::KeyRotation
}
Action::SendMessage((msg, cnf)) => {
let msg_id = msg.sign_and_return(cnf).await;
Action::SendMessage((db, cnf, msg)) => {
let msg_id = msg.sign_and_return(db, cnf).await;
ActionResult::MessageSent(msg_id)
}
}

View file

@ -1,7 +1,12 @@
use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use ed25519_dalek::SigningKey;
use ed25519_dalek::ed25519::SignatureEncoding;
use ed25519_dalek::{Signer, SigningKey as Ed25519SigningKey};
use rand::thread_rng;
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey};
use rsa::pkcs1v15::SigningKey as RsaSigningKey;
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey};
use rsa::sha2::Sha256;
use rsa::signature::hazmat::PrehashSigner;
use rsa::{RsaPrivateKey, RsaPublicKey};
use std::str::FromStr;
@ -32,10 +37,20 @@ impl Algorithm {
}
}
pub fn sign(&self, encoded_pk: &str, data: &[u8]) -> String {
pub fn sign(&self, encoded_pk: &str, data: &[u8]) -> Result<Vec<u8>> {
let pk = general_purpose::STANDARD.decode(encoded_pk)?;
match self {
Self::Ed25519Sha256 => String::new(),
Self::Rsa2048Sha256 | Self::Rsa3072Sha256 | Self::Rsa4096Sha256 => String::new(),
Self::Ed25519Sha256 => {
let signing_key = Ed25519SigningKey::from_bytes(pk.as_slice().try_into()?);
let signature = signing_key.try_sign(data)?;
Ok(signature.to_vec())
}
Self::Rsa2048Sha256 | Self::Rsa3072Sha256 | Self::Rsa4096Sha256 => {
let private_key = RsaPrivateKey::from_pkcs8_der(&pk)?;
let signing_key = RsaSigningKey::<Sha256>::new_with_prefix(private_key);
let signature = signing_key.sign_prehash(data)?;
Ok(signature.to_vec())
}
}
}
}
@ -73,7 +88,7 @@ impl FromStr for Algorithm {
fn gen_ed25519_kp() -> (String, String) {
let mut csprng = thread_rng();
let priv_key = SigningKey::generate(&mut csprng);
let priv_key = Ed25519SigningKey::generate(&mut csprng);
let pub_key = priv_key.verifying_key();
let priv_key = general_purpose::STANDARD.encode(priv_key.to_bytes());
let pub_key = general_purpose::STANDARD.encode(pub_key.to_bytes());

View file

@ -38,6 +38,14 @@ WHERE
AND published IS FALSE
ORDER BY not_after DESC
LIMIT 1";
pub const SELECT_LATEST_SIGNING_KEY: &str = "SELECT selector, private_key
FROM key_db
WHERE
sdid = $1
AND algorithm = $2
AND published IS FALSE
ORDER BY not_after DESC
LIMIT 1";
pub const SELECT_NEAREST_KEY_PUBLICATION: &str = "SELECT revocation
FROM key_db
WHERE published IS FALSE

View file

@ -9,6 +9,7 @@ mod key;
mod logs;
mod message;
mod parsed_message;
mod signature;
mod stdin_reader;
use action::{new_action, Action, ActionResult};
@ -38,6 +39,8 @@ const DEFAULT_LIB_DIR: &str = env!("VARLIBDIR");
const DEFAULT_MSG_SIZE: usize = 1024 * 1024;
const KEY_CHECK_MIN_DELAY: u64 = 60 * 60 * 3;
const LOG_LEVEL_ENV_VAR: &str = "OPENSMTPD_FILTER_DKIMOUT_LOG_LEVEL";
const SIG_RETRY_NB_RETRY: usize = 10;
const SIG_RETRY_SLEEP_TIME: u64 = 10;
#[macro_export]
macro_rules! display_bytes {
@ -117,7 +120,7 @@ async fn main_loop(cnf: &config::Config, db: &SqlitePool) {
} else {
log::debug!("message ready: {msg_id}");
if let Some(m) = messages.remove(&msg_id) {
actions.push(new_action(Action::SendMessage((m, cnf))));
actions.push(new_action(Action::SendMessage((db, cnf, m))));
}
}
}
@ -127,7 +130,7 @@ async fn main_loop(cnf: &config::Config, db: &SqlitePool) {
if !entry.is_end_of_message() {
messages.insert(msg_id.clone(), msg);
} else {
actions.push(new_action(Action::SendMessage((msg, cnf))));
actions.push(new_action(Action::SendMessage((db, cnf, msg))));
}
}
}

View file

@ -1,7 +1,9 @@
use crate::config::Config;
use crate::entry::Entry;
use crate::parsed_message::ParsedMessage;
use crate::signature::Signature;
use anyhow::Result;
use sqlx::SqlitePool;
use tokio::io::{AsyncWriteExt, BufWriter};
pub const RETURN_SEP: &[u8] = b"|";
@ -52,8 +54,12 @@ impl Message {
self.nb_lines
}
pub async fn sign_and_return(&self, cnf: &Config) -> String {
log::trace!("content: {}", crate::display_bytes!(&self.content));
pub async fn sign_and_return(&self, db: &SqlitePool, cnf: &Config) -> String {
let msg_id = get_msg_id(&self.session_id, &self.token);
log::trace!(
"{msg_id}: content: {}",
crate::display_bytes!(&self.content)
);
match ParsedMessage::from_bytes(&self.content) {
Ok(parsed_msg) => {
log::trace!("mail parsed");
@ -75,16 +81,31 @@ impl Message {
"ParsedMessage: body: {}",
crate::display_bytes!(parsed_msg.body)
);
// TODO: sign the message using DKIM
match Signature::new(db, cnf, &parsed_msg).await {
Ok(signature) => {
let sig_header = signature.get_header();
if let Err(err) = self.print_sig_header(&sig_header).await {
log::error!("{msg_id}: unable to add the signature header: {err}");
}
}
Err(err) => log::error!("{msg_id}: unable to sign message: {err}"),
}
}
Err(err) => {
log::error!("{msg_id}: unable to parse message: {err}");
}
}
if let Err(err) = self.print_msg().await {
log::error!("unable to write message: {err}");
log::error!("{msg_id}: unable to write message: {err}");
}
get_msg_id(&self.session_id, &self.token)
msg_id
}
async fn print_sig_header(&self, sig_header: &str) -> Result<()> {
for line in sig_header.split("\r\n") {
self.print_line(line.as_bytes()).await?;
}
Ok(())
}
async fn print_msg(&self) -> Result<()> {

155
src/signature.rs Normal file
View file

@ -0,0 +1,155 @@
use crate::algorithm::Algorithm;
use crate::canonicalization::Canonicalization;
use crate::config::Config;
use crate::parsed_message::{ParsedHeader, ParsedMessage};
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose, Engine as _};
use sha2::{Digest, Sha256};
use sqlx::types::time::OffsetDateTime;
use sqlx::SqlitePool;
use tokio::time::{sleep, Duration};
pub struct Signature {
algorithm: Algorithm,
canonicalization: Canonicalization,
selector: String,
sdid: String,
timestamp: i64,
headers: Vec<String>,
body_hash: Vec<u8>,
signature: Vec<u8>,
}
impl Signature {
pub async fn new(db: &SqlitePool, cnf: &Config, msg: &ParsedMessage<'_>) -> Result<Self> {
let algorithm = cnf.algorithm();
let sdid = get_sdid(cnf, msg)?;
let (selector, signing_key) = get_db_data(db, &sdid, algorithm).await?;
let mut sig = Self {
algorithm,
canonicalization: cnf.canonicalization(),
selector,
sdid,
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
headers: get_headers(cnf, msg),
body_hash: Vec::new(),
signature: Vec::new(),
};
sig.compute_body_hash::<Sha256>(msg);
let header_hash = sig.compute_header_hash::<Sha256>(msg);
sig.signature = algorithm.sign(&signing_key, &header_hash)?;
Ok(sig)
}
pub fn get_header(&self) -> String {
format!(
"DKIM-Signature: v=1; a={algorithm}; c={canonicalization}; d={sdid};\r\n\tt={timestamp}; s={selector};\r\n\th={headers};\r\n\tbh={body_hash};\r\n\tb={signature}",
algorithm=self.algorithm.display(),
canonicalization=self.canonicalization.to_string(),
selector=self.selector,
sdid=self.sdid,
timestamp=self.timestamp,
headers=self.headers.join(":"),
body_hash=general_purpose::STANDARD.encode(&self.body_hash),
signature=general_purpose::STANDARD.encode(&self.signature),
)
}
fn compute_body_hash<H: Digest>(&mut self, msg: &ParsedMessage<'_>) {
let mut hasher = H::new();
let body = self.canonicalization.process_body(msg.body);
hasher.update(&body);
self.body_hash = hasher.finalize().to_vec();
}
fn compute_header_hash<H: Digest>(&mut self, msg: &ParsedMessage<'_>) -> Vec<u8> {
let mut hasher = H::new();
for header_name in &self.headers {
if let Some(raw_header) = get_header(msg, header_name) {
let header = self.canonicalization.process_header(raw_header.raw);
hasher.update(&header);
}
}
hasher.update(self.get_header().as_bytes());
hasher.finalize().to_vec()
}
}
fn get_sdid(cnf: &Config, msg: &ParsedMessage<'_>) -> Result<String> {
if let Some(header) = get_header(msg, "from") {
if let Some(arb_pos) = header.value.iter().rposition(|&c| c == b'@') {
let name = &header.value[arb_pos + 1..];
let end_pos = name
.iter()
.position(|&c| c == b'>')
.unwrap_or(name.len() - 2);
if let Ok(sdid) = String::from_utf8(name[..end_pos].to_vec()) {
if cnf.domains().contains(&sdid) {
return Ok(sdid);
} else {
return Err(anyhow!(
"unable to sign for a domain outside of the configured list: {sdid}"
));
}
}
}
}
Err(anyhow!("unable to determine the SDID"))
}
fn get_headers(cnf: &Config, msg: &ParsedMessage<'_>) -> Vec<String> {
let nb_headers = cnf.headers().len() + cnf.headers_optional().len();
let mut lst = Vec::with_capacity(nb_headers);
for header_name in cnf.headers() {
if let Some(name) = get_header_name(msg, header_name) {
lst.push(name);
} else {
lst.push(header_name.to_string());
}
}
for header_name in cnf.headers_optional() {
if let Some(name) = get_header_name(msg, header_name) {
lst.push(name);
}
}
lst.sort();
lst
}
fn get_header_name(msg: &ParsedMessage<'_>, header_name: &str) -> Option<String> {
match get_header(msg, header_name) {
Some(header) => String::from_utf8(header.name.to_vec()).ok(),
None => None,
}
}
fn get_header<'a>(
msg: &'a ParsedMessage<'a>,
header_name: &'a str,
) -> Option<&'a ParsedHeader<'a>> {
let header_name = header_name.to_lowercase();
msg.headers.iter().find(|&header| header.name_lower == header_name)
}
async fn get_db_data(
db: &SqlitePool,
sdid: &str,
algorithm: Algorithm,
) -> Result<(String, String)> {
let mut ctn = 0;
loop {
let res: Option<(String, String)> = sqlx::query_as(crate::db::SELECT_LATEST_SIGNING_KEY)
.bind(sdid)
.bind(algorithm.to_string())
.fetch_optional(db)
.await?;
if let Some((selector, private_key)) = res {
return Ok((selector, private_key));
}
if ctn == crate::SIG_RETRY_NB_RETRY {
return Err(anyhow!("unable to retrieve key material"));
}
ctn += 1;
sleep(Duration::from_secs(crate::SIG_RETRY_SLEEP_TIME)).await;
}
}