Sign in

    mbt-mailauth

    Pure MoonBit email authentication engine: DKIM signature verification, SPF evaluation, DMARC alignment and Authentication-Results output.

    email
    dkim
    spf
    dmarc
    authentication
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    10 hours ago
    Downloads
    12

    Dependencies

    #mbt-mailauth

    纯 MoonBit 实现的邮件身份验证引擎:DKIM 验签 + SPF 求值 + DMARC 对齐 + Authentication-Results。

    核心不依赖任何第三方邮件或密码学实现,DNS 通过 trait 注入,因此同一封邮件在任何机器上都能得到同一结论。

    #特性

    • DKIM(RFC 6376):simple / relaxed 双规范化、h= 头部选择与 oversigning、l= 截断、bh= 比对、RSA-SHA256 验签、多签名、x= 过期、p= 撤销
    • SPF(RFC 7208):all include a mx ptr ip4 ip6 exists 全机制、redirect / exp 修饰符、宏展开与变换器、10 次 DNS 查询上限、2 次 void lookup 上限
    • DMARC(RFC 7489):relaxed / strict 对齐、组织域回退、sp 子域策略、pct 采样、聚合报告 XML
    • Authentication-Results(RFC 8601):可直接交给真实邮件栈消费
    • 可解释:每个结论都带证据(规范化字节数、命中的机制、DNS key 来源)
    • 可测试:离线 zone fixture,无网络也能复现;跨实现语料由 Node/OpenSSL 签名

    #安装

    moon add Mr-Houjie/mbt-mailauth

    #CLI

    moon run cmd/main -- message.eml --ip 192.0.2.5 --zone zone.txt

    zone.txt 每行一条记录:

    test._domainkey.example.com TXT v=DKIM1; k=rsa; p=MIIBIjAN... example.com TXT v=spf1 ip4:192.0.2.0/24 -all _dmarc.example.com TXT v=DMARC1; p=reject

    选项:

    --ip <addr> SMTP 对端 IP,SPF 必需 --zone <file> 离线 DNS zone --mail-from <domain> MAIL FROM 域,默认取 From 头 --helo <name> HELO 名,供 SPF 宏使用 --format <fmt> text | json | header --now <unix> 校验签名 x= 过期时间 --sample <0-99> DMARC pct 采样值

    退出码:0 通过,1 DMARC 未通过,2 用法或读写错误。

    输出示例:

    dkim: pass d=example.com s=test reason: signature verified - body-hash: canonical body 33 bytes, bh=7x9e9eVKz/7ajCJi+nmVKcjCtQaN2/fohG/wOvpvNnE= - header-hash: signed headers: 7, input 423 bytes - key: test._domainkey.example.com: rsa 2048 bits spf: pass domain=example.com mechanism: ip4:192.0.2.0/24 dmarc: pass domain=example.com policy: reject, disposition: none aligned: dkim=true, spf=true

    #库 API

    let zone = @Mr-Houjie/mbt-mailauth.Zone::from_text(zone_text)
    let report = @Mr-Houjie/mbt-mailauth.verify_message(
    zone,
    raw_email,
    "192.0.2.5",
    "", // mail_from,空表示取 From 头
    "", // helo
    0, // now,0 表示不校验过期
    -1, // sample,负数表示 pct 使用完整策略
    )

    println(@Mr-Houjie/mbt-mailauth.render_text(report))
    println(@Mr-Houjie/mbt-mailauth.render_json(report))
    println(@Mr-Houjie/mbt-mailauth.authentication_results(report, "mx.example.com"))

    也可以按层调用:verify_dkim / evaluate_spf / evaluate_dmarc。

    #架构

    types.mbt 判定模型:状态 + 证据 + 原因 message.mbt 原始字节切分(保留头部原始字节) canon.mbt DKIM simple / relaxed 规范化 sha256.mbt 自研 SHA-256(NIST 向量) rsa.mbt 最小 DER/SPKI 解析 + PKCS#1 v1.5 验签 dkim.mbt 签名头解析、头部选择、验签 spf.mbt 记录解析、机制、宏、限额、求值 dmarc.mbt 对齐、策略、组织域、聚合报告 render.mbt text / json / Authentication-Results verify.mbt 编排:一封邮件 → 完整报告 dns.mbt Resolver trait 与离线 Zone selftest.mbt 19 条固定语料 cmd/main 命令行 cmd/verify 独立自检入口 tests/tools/ 跨实现向量生成脚本(Node/OpenSSL)

    #测试

    # 独立自检(Windows 下 moon test 可能受嵌套进程限制) moon run cmd/verify # 标准测试(Linux / macOS) moon test

    DKIM 语料由 tests/tools/gen_dkim_vector.js 用 Node/OpenSSL 生成密钥并签名, 本项目只负责验签;篡改正文、篡改头部两个样本必须被拒绝。重新生成会更换密钥:

    node tests/tools/gen_dkim_vector.js

    #边界与不做的事

    • 不做任何网络 I/O:DNS 必须由调用方注入;ptr 机制的反向解析同样走注入
    • 不做 MTA / SMTP 投递,不做邮件正文渲染与附件解码
    • 聚合报告只生成 XML,不负责投递
    • RFC 6376 强制要求的 rsa-sha256 已完整实现;Ed25519(RFC 8463 扩展)暂未覆盖
    • 组织域基于内置常见多级后缀表,未内置完整 PSL

    #许可证

    MIT

    Resolver

    pub(open) trait Resolver {
    fn txt(Self, String) -> Array[String]
    fn a(Self, String) -> Array[String]
    fn aaaa(Self, String) -> Array[String]
    fn mx(Self, String) -> Array[String]
    fn ptr(Self, String) -> Array[String]
    }

    只读 DNS 解析器。实现者可以是离线 zone,也可以是真实的 DNS 客户端。

    AuthReport

    pub struct AuthReport {
    dkim : Array[DkimResult]
    spf : SpfResult?
    dmarc : DmarcResult?
    }

    一封邮件的完整认证结果。

    AuthStatus

    pub enum AuthStatus {
    Pass
    Fail
    SoftFail
    None_
    Neutral
    TempError
    PermError
    } derive(Eq)

    认证状态。对应 RFC 8601 的 result 取值。

    AuthStatus::label

    fn AuthStatus::label(self : AuthStatus) -> String

    判定状态的短名称,用于渲染。

    CanonMode

    pub enum CanonMode {
    Simple
    Relaxed
    }

    DkimResult

    pub struct DkimResult {
    domain : String
    selector : String
    algorithm : String
    status : AuthStatus
    reason : String
    evidence : Array[Evidence]
    }

    单条 DKIM 签名的验签结果。

    DkimSignature

    pub struct DkimSignature {
    domain : String
    selector : String
    algorithm : String
    header_canon : CanonMode
    body_canon : CanonMode
    signed_headers : Array[String]
    body_hash : String
    signature : String
    identity : String
    length_limit : Int?
    expiration : Int?
    }

    DmarcAlign

    pub enum DmarcAlign {
    AlignRelaxed
    AlignStrict
    }

    DmarcPolicy

    pub struct DmarcPolicy {
    policy : String
    subdomain_policy : String?
    pct : Int
    adkim : DmarcAlign
    aspf : DmarcAlign
    rua : Array[String]
    ruf : Array[String]
    }

    DmarcResult

    pub struct DmarcResult {
    domain : String
    policy : String
    disposition : String
    dkim_aligned : Bool
    spf_aligned : Bool
    status : AuthStatus
    reason : String
    }

    DMARC 对齐与策略结果。

    Evidence

    pub struct Evidence {
    kind : String
    label : String
    detail : String
    }

    一条判定证据。

    HeaderField

    pub struct HeaderField {
    name : String
    raw : Bytes
    }

    MacroCtx

    pub struct MacroCtx {
    sender : String
    domain : String
    ip : String
    helo : String
    }

    宏展开上下文。

    Message

    pub struct Message {
    headers : Array[HeaderField]
    body : Bytes
    }

    Message::headers_named

    fn Message::headers_named(self : Message, name : String) -> Array[HeaderField]

    取某个字段名的全部字段(按出现顺序,从顶部到底部)。

    RsaPublicKey

    pub struct RsaPublicKey {
    modulus :
    BigInt

    exponent :
    BigInt

    byte_len : Int
    }

    SpfCounter

    type SpfCounter

    DNS 查询计数与限额。

    SpfKind

    pub enum SpfKind {
    All
    Include
    A
    Mx
    Ptr
    Ip4
    Ip6
    Exists
    Redirect
    Exp
    Unknown
    } derive(Eq)

    SpfOutcome

    type SpfOutcome

    单域名求值结果。

    SpfQualifier

    pub enum SpfQualifier {
    Plus
    Minus
    Tilde
    Question
    } derive(Eq)

    SpfRecord

    pub struct SpfRecord {
    terms : Array[SpfTerm]
    }

    SpfResult

    pub struct SpfResult {
    domain : String
    identity : String
    status : AuthStatus
    qualifier : String
    mechanism : String
    reason : String
    dns_lookups : Int
    void_lookups : Int
    }

    SPF 求值结果。

    SpfTerm

    pub struct SpfTerm {
    qualifier : SpfQualifier
    kind : SpfKind
    value : String
    cidr4 : Int?
    cidr6 : Int?
    raw : String
    }

    Zone

    pub struct Zone {
    records : Array[(String, String, Array[String])]
    }

    离线 DNS zone。按「名称 + 记录类型」保存记录,供测试与 CI 使用。
    impl Resolver for Zone

    Zone::add

    fn Zone::add(self : Zone, name : String, rtype : String, values : Array[String]) -> Unit

    添加一条记录。名称大小写与末尾点会被归一化。

    Zone::from_text

    fn Zone::from_text(text : StringView) -> Zone

    从文本加载 zone。每行格式:name TYPE value,value 可以包含空格。

    Zone::new

    fn Zone::new() -> Zone

    ascii_lower

    fn ascii_lower(text : StringView) -> String

    字段名转小写。

    authentication_results

    fn authentication_results(report : AuthReport, authserv_id : String) -> String

    生成 RFC 8601 的 Authentication-Results 头(不含字段名)。

    bytes_equal

    fn bytes_equal(a : BytesView, b : BytesView) -> Bool

    比较两个字节串是否相等。

    canonicalize_body

    fn canonicalize_body(body : Bytes, mode : CanonMode) -> Bytes

    规范化正文。

    canonicalize_header

    fn canonicalize_header(field : HeaderField, mode : CanonMode) -> Bytes

    规范化一个头部字段,输出总是以 CRLF 结尾。

    dkim_expected_body_hash

    let dkim_expected_body_hash : String

    dkim_relaxed_eml

    let dkim_relaxed_eml : String

    dkim_tampered_body_eml

    let dkim_tampered_body_eml : String

    dkim_tampered_header_eml

    let dkim_tampered_header_eml : String

    dkim_zone_text

    let dkim_zone_text : String

    dmarc_aggregate_report

    fn dmarc_aggregate_report(report : AuthReport, org_name : String, email : String, begin : Int, end : Int, domain : String) -> String

    生成 DMARC 聚合报告(最小可用 XML)。

    evaluate_dmarc

    fn[R : Resolver] evaluate_dmarc(resolver : R, from_domain : String, dkim : Array[DkimResult], spf : SpfResult?, sample? : Int) -> DmarcResult

    DMARC 求值。sample 用于 pct 采样:0 <= sample < pct 时才执行策略。

    evaluate_spf

    fn[R : Resolver] evaluate_spf(resolver : R, domain : String, ip : String, helo? : String, sender? : String) -> SpfResult

    对发件域做 SPF 求值。

    expand_macro

    fn expand_macro(pattern : String, ctx : MacroCtx, current_domain : String) -> String

    展开 SPF 宏。current_domain 是正在求值的域,用于 %{d}。

    from_domain

    fn from_domain(message : Message) -> String?

    从 From 头取域名。

    normalize_crlf

    fn normalize_crlf(input : BytesView) -> Bytes

    把行尾统一成 CRLF:裸 LF 补成 CRLF,孤立的 CR 保留。

    normalize_name

    fn normalize_name(name : String) -> String

    把域名转成小写并去掉末尾的点。

    organizational_domain

    fn organizational_domain(domain : String) -> String

    计算组织域:a.b.example.co.uk → example.co.uk。

    parse_dkim_signature

    fn parse_dkim_signature(field : HeaderField) -> DkimSignature?

    解析签名头。返回 None 表示缺少必填标签。

    parse_dmarc_record

    fn parse_dmarc_record(text : String) -> DmarcPolicy?

    解析 DMARC 记录。返回 None 表示不是 v=DMARC1。

    parse_ipv4

    fn parse_ipv4(text : String) -> Array[Int]?

    解析 IPv4,返回四个八位组。

    parse_ipv6

    fn parse_ipv6(text : String) -> Array[Int]?

    解析 IPv6,返回八个 16 位分组。

    parse_message

    fn parse_message(input : BytesView) -> Message

    解析一封邮件:头部按字段切开,正文保留原始字节。

    parse_spf_record

    fn parse_spf_record(text : String) -> SpfRecord?

    解析 SPF 记录。返回 None 表示不是 v=spf1 记录。

    parse_tags

    fn parse_tags(text : String) -> Array[(String, String)]

    解析 k=v; k=v 形式的标签列表。

    render_json

    fn render_json(report : AuthReport) -> String

    稳定的 json 输出,字段顺序固定,方便 CI 对拍。

    render_text

    fn render_text(report : AuthReport) -> String

    人类可读的 text 输出。

    rsa_public_key_from_der

    fn rsa_public_key_from_der(der : Bytes) -> RsaPublicKey?

    解析 DKIM p= 里的公钥:优先按 SubjectPublicKeyInfo,失败再按裸 RSAPublicKey。

    两种形式在真实邮件里都能遇到,因此不能只认一种。

    rsa_verify_sha256

    fn rsa_verify_sha256(key : RsaPublicKey, message : BytesView, signature : BytesView) -> Bool

    RSA PKCS#1 v1.5 + SHA-256 验签。

    rsa_verify_sha256_base64

    fn rsa_verify_sha256_base64(public_key_b64 : String, message : BytesView, signature_b64 : String) -> Bool

    用 base64 公钥与 base64 签名做一次验签,供语料与自检使用。

    run_selftest

    fn run_selftest() -> Array[String]

    selftest_case_count

    fn selftest_case_count() -> Int

    sha256

    fn sha256(data : BytesView) -> Bytes

    计算 SHA-256 摘要,返回 32 字节。

    strip_ws

    fn strip_ws(text : StringView) -> String

    去掉字符串里的全部空白,用于 base64 与哈希比较。

    trim_wsp

    fn trim_wsp(text : StringView) -> String

    去掉首尾的 WSP。

    verify_dkim

    fn[R : Resolver] verify_dkim(resolver : R, message : Message, signature_field : HeaderField, now? : Int) -> DkimResult

    验证一封邮件里的某一条 DKIM-Signature。

    verify_dkim_all

    fn[R : Resolver] verify_dkim_all(resolver : R, message : Message, now? : Int) -> Array[DkimResult]

    验证邮件里的所有 DKIM-Signature,按出现顺序返回。

    verify_message

    fn[R : Resolver] verify_message(resolver : R, raw : BytesView, ip : String, mail_from : String, helo : String, now : Int, sample : Int) -> AuthReport

    对一封邮件做完整认证。

    ip 是建立 SMTP 连接的对端地址,SPF 必须依赖它,邮件本身不携带。 now = 0 表示不校验签名过期时间;sample < 0 表示 DMARC pct 使用完整策略。

    xml_escape

    fn xml_escape(text : StringView) -> String

    XML 最小转义。