mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-11-04 08:14:10 +00:00
236 lines
8.1 KiB
Rust
236 lines
8.1 KiB
Rust
/*
|
|
* Copyright (c) 2023 Stalwart Labs Ltd.
|
|
*
|
|
* This file is part of Stalwart Mail Server.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
* in the LICENSE file at the top-level directory of this distribution.
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* You can be released from the requirements of the AGPLv3 license by
|
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
|
* for more details.
|
|
*/
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use directory::core::config::ConfigDirectory;
|
|
use mail_auth::{
|
|
common::{parse::TxtRecordParser, verify::DomainKey},
|
|
spf::Spf,
|
|
};
|
|
use store::Stores;
|
|
use utils::config::{Config, DynValue};
|
|
|
|
use crate::smtp::{
|
|
inbound::{TestMessage, TestQueueEvent},
|
|
session::{TestSession, VerifyResponse},
|
|
ParseTestConfig, TestConfig, TestSMTP,
|
|
};
|
|
use smtp::{
|
|
config::{
|
|
auth::ConfigAuth, ConfigContext, EnvelopeKey, IfBlock, MaybeDynValue, VerifyStrategy,
|
|
},
|
|
core::{Session, SMTP},
|
|
};
|
|
|
|
const SIGNATURES: &str = "
|
|
[signature.rsa]
|
|
private-key = '''
|
|
-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm1agrbcnSk5oIaEfM
|
|
ZLUR/X8gPzoiNHZcfMZEVR6bAytxUhc5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIa
|
|
UmSACj0dL2/KwW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bWPZNkT4
|
|
NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0TqgXdtw6A4Sdsp97VXT9fkPW
|
|
9rso3lrkpsl/9EQ1mR/DWK6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Ae
|
|
d4l9WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQABAoIBAFPChEi/OvnulReB
|
|
ECQWhOUYuNKlFKQU++2YEvZJ4+bMn5UgnE7wfJ1pj2Pr9xlfALz+OMHNrjMxGbaV
|
|
KzdrT2uCkYcf78XjnhuH9gKIiXDUv4L4N+P3u6w8yOx4bFgOS9IjS53yDOPM7SC5
|
|
g6dIg5aigHaHlffqIuFFv4yQMI/+Ai+zBKxS7wRhxK/7nnAuo28fe5MEdp57ho9/
|
|
AGlDNsdg9zCgjwhokwFE3+AaD+bkUFm4gQ1XjkUFrlmnQn8vDQ0i9toEWhCj+UPY
|
|
iOKL63MJnr90MXTXWLHoFj99wBp//mYygbF9Lj8fa28/oa8LWp3Jhb7QeMgH46iv
|
|
3aLHbTECgYEA5M2dAw+nyMw9vYlkMejhwObKYP8Mr/6zcGMLCalYvRJM5iUAM0JI
|
|
H6sM6pV9/nv167cbKocj3xYPdtE7FPOn4132MLM8Ne1f8nPE64Qrcbj5WBXvLnU8
|
|
hpWbwe2Z8h7UUMKx6q4F1/TXYkc3ScxYwfjM4mP/pLsAOgVzRSEEgrUCgYEA1qNQ
|
|
xaQHNWZ1O8WuTnqWd5JSsic6iURAmUcLeFDZY2PWhVoaQ8L/xMQhDYs1FIbLWArW
|
|
4Qq3Ibu8AbSejAKuaJz7Uf26PX+PYVUwAOO0qamCJ8d/qd6So7qWMDyAY2yXI39Y
|
|
1nMqRjr7bkEsggAZao7BKqA7ZtmogjOusBT38iUCgYEA06agJ8TDoKvOMRZ26PRU
|
|
YO0dKLzGL8eclcoI29cbj0rud7aiiMg3j5PbTuUat95TjsjDCIQaWrM9etvxm2AJ
|
|
Xfn9Uu96MyhyKQWOk46f4YMKpMElkARDCPw8KRhx39dE77AqhLyWCz8iPndCXbH6
|
|
KPTOEl4OjYOuof2Is9nnIkECgYBh948RdsnXhNlzm8nwhiGRmBbou+EK8D0v+O5y
|
|
Tyy6IcKzgSnFzgZh8EdJ4EUtBk1f9SqY8wQdgIvSl3daXorusuA/TzkngsaV3YUY
|
|
ktZOLlF7CKLrjOyPkMWmZKcROmpNyH1q/IvKHHfQnizLdXIkYd4nL5WNX0F7lE1i
|
|
j1+QhQKBgB2lviBK7rJFwlFYdQUP1NAN2dKxMZk8uJS8JglHrM0+8nRI83HbTdEQ
|
|
vB0ManEKBkbS4T5n+gRtdEqKSDmWDTXDlrBfcdCHNQLwYtBpOotCqQn/AmfjcPBl
|
|
byAbwh4+HiZ5JISoRZpiZqy67aJNVoXmdtb/E9mi7ozzytpxMNql
|
|
-----END RSA PRIVATE KEY-----'''
|
|
domain = 'example.com'
|
|
selector = 'rsa'
|
|
headers = ['From', 'To', 'Date', 'Subject', 'Message-ID']
|
|
algorithm = 'rsa-sha256'
|
|
canonicalization = 'simple/relaxed'
|
|
expire = '10d'
|
|
set-body-length = true
|
|
report = true
|
|
|
|
[signature.ed]
|
|
public-key = '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='
|
|
private-key = '-----BEGIN PRIVATE KEY-----
|
|
nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=
|
|
-----END PRIVATE KEY-----'
|
|
domain = 'example.com'
|
|
selector = 'ed'
|
|
headers = ['From', 'To', 'Date', 'Subject', 'Message-ID']
|
|
algorithm = 'ed25519-sha256'
|
|
canonicalization = 'relaxed/simple'
|
|
set-body-length = false
|
|
";
|
|
|
|
const DIRECTORY: &str = r#"
|
|
[directory."local"]
|
|
type = "memory"
|
|
|
|
[[directory."local".principals]]
|
|
name = "john"
|
|
description = "John Doe"
|
|
secret = "secret"
|
|
email = ["jdoe@example.com"]
|
|
"#;
|
|
|
|
#[tokio::test]
|
|
async fn sign_and_seal() {
|
|
let mut core = SMTP::test();
|
|
|
|
// Create temp dir for queue
|
|
let mut qr = core.init_test_queue("smtp_sign_test");
|
|
|
|
// Add SPF, DKIM and DMARC records
|
|
core.resolvers.dns.txt_add(
|
|
"mx.example.com",
|
|
Spf::parse(b"v=spf1 ip4:10.0.0.1 ip4:10.0.0.2 -all").unwrap(),
|
|
Instant::now() + Duration::from_secs(5),
|
|
);
|
|
core.resolvers.dns.txt_add(
|
|
"example.com",
|
|
Spf::parse(b"v=spf1 ip4:10.0.0.1 -all").unwrap(),
|
|
Instant::now() + Duration::from_secs(5),
|
|
);
|
|
core.resolvers.dns.txt_add(
|
|
"ed._domainkey.scamorza.org",
|
|
DomainKey::parse(
|
|
concat!(
|
|
"v=DKIM1; k=ed25519; ",
|
|
"p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.unwrap(),
|
|
Instant::now() + Duration::from_secs(5),
|
|
);
|
|
core.resolvers.dns.txt_add(
|
|
"rsa._domainkey.manchego.org",
|
|
DomainKey::parse(
|
|
concat!(
|
|
"v=DKIM1; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ",
|
|
"KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt",
|
|
"IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v",
|
|
"/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
|
|
"tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.unwrap(),
|
|
Instant::now() + Duration::from_secs(5),
|
|
);
|
|
|
|
let directory = Config::new(DIRECTORY)
|
|
.unwrap()
|
|
.parse_directory(&Stores::default(), None)
|
|
.unwrap();
|
|
let config = &mut core.session.config.rcpt;
|
|
config.directory = IfBlock::new(Some(MaybeDynValue::Static(
|
|
directory.directories.get("local").unwrap().clone(),
|
|
)));
|
|
|
|
let config = &mut core.session.config;
|
|
config.data.add_auth_results = IfBlock::new(true);
|
|
config.data.add_date = IfBlock::new(true);
|
|
config.data.add_message_id = IfBlock::new(true);
|
|
config.data.add_received = IfBlock::new(true);
|
|
config.data.add_return_path = IfBlock::new(true);
|
|
config.data.add_received_spf = IfBlock::new(true);
|
|
|
|
let config = &mut core.mail_auth;
|
|
let ctx = ConfigContext::new(&[]).parse_signatures();
|
|
config.spf.verify_ehlo = IfBlock::new(VerifyStrategy::Relaxed);
|
|
config.spf.verify_mail_from = config.spf.verify_ehlo.clone();
|
|
config.dkim.verify = config.spf.verify_ehlo.clone();
|
|
config.arc.verify = config.spf.verify_ehlo.clone();
|
|
config.dmarc.verify = config.spf.verify_ehlo.clone();
|
|
config.dkim.sign = "['rsa']"
|
|
.parse_if::<Vec<DynValue<EnvelopeKey>>>(&ctx)
|
|
.map_if_block(&ctx.signers, "", "")
|
|
.unwrap();
|
|
config.arc.seal = "'ed'"
|
|
.parse_if::<Option<DynValue<EnvelopeKey>>>(&ctx)
|
|
.map_if_block(&ctx.sealers, "", "")
|
|
.unwrap();
|
|
|
|
// Test DKIM signing
|
|
let mut session = Session::test(core);
|
|
session.data.remote_ip = "10.0.0.2".parse().unwrap();
|
|
session.eval_session_params().await;
|
|
session.ehlo("mx.example.com").await;
|
|
session
|
|
.send_message(
|
|
"bill@foobar.org",
|
|
&["jdoe@example.com"],
|
|
"test:no_dkim",
|
|
"250",
|
|
)
|
|
.await;
|
|
qr.read_event()
|
|
.await
|
|
.unwrap_message()
|
|
.read_lines()
|
|
.assert_contains(
|
|
"DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com; c=simple/relaxed;",
|
|
);
|
|
|
|
// Test ARC verify and seal
|
|
session
|
|
.send_message("bill@foobar.org", &["jdoe@example.com"], "test:arc", "250")
|
|
.await;
|
|
qr.read_event()
|
|
.await
|
|
.unwrap_message()
|
|
.read_lines()
|
|
.assert_contains("ARC-Seal: i=3; a=ed25519-sha256; s=ed; d=example.com; cv=pass;")
|
|
.assert_contains(
|
|
"ARC-Message-Signature: i=3; a=ed25519-sha256; s=ed; d=example.com; c=relaxed/simple;",
|
|
);
|
|
}
|
|
|
|
pub trait TextConfigContext<'x> {
|
|
fn parse_signatures(self) -> ConfigContext<'x>;
|
|
}
|
|
|
|
impl<'x> TextConfigContext<'x> for ConfigContext<'x> {
|
|
fn parse_signatures(mut self) -> Self {
|
|
Config::new(SIGNATURES)
|
|
.unwrap()
|
|
.parse_signatures(&mut self)
|
|
.unwrap();
|
|
self
|
|
}
|
|
}
|