// DKIM 验签(RFC 6376)。
//
// 流程:解析 DKIM-Signature 标签 → 规范化正文并比对 bh → 按 h= 选择头部并规范化
// → 拼接(含去掉 b= 值的签名头)→ 取 DNS key → RSA 验签。
///|
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?
}
///|
fn is_ws_char(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
///|
/// 去掉字符串里的全部空白,用于 base64 与哈希比较。
pub fn strip_ws(text : StringView) -> String {
let out = StringBuilder()
for c in text {
if !is_ws_char(c) {
out.write_char(c)
}
}
out.to_string()
}
///|
/// 解析 `k=v; k=v` 形式的标签列表。
pub fn parse_tags(text : String) -> Array[(String, String)] {
let out : Array[(String, String)] = []
for raw_part in text.split(";") {
let part = raw_part.to_owned()
match part.split_once("=") {
Some((k, v)) => out.push((ascii_lower(trim_wsp(k)), trim_wsp(v)))
None => ()
}
}
out
}
///|
fn tag_get(tags : Array[(String, String)], name : String) -> String? {
for tag in tags {
if tag.0 == name {
return Some(tag.1)
}
}
None
}
///|
/// 取字段冒号之后的值文本,并把折叠换行替换成空格。
fn header_value_text(field : HeaderField) -> String {
let raw = field.raw
let n = raw.length()
let mut colon = 0
while colon < n && raw[colon] != b':' {
colon = colon + 1
}
let start = if colon < n { colon + 1 } else { n }
let out : Array[Byte] = []
for i in start.. DkimSignature? {
let tags = parse_tags(header_value_text(field))
let version = tag_get(tags, "v")
let algorithm = tag_get(tags, "a")
let domain = tag_get(tags, "d")
let selector = tag_get(tags, "s")
let signed = tag_get(tags, "h")
let body_hash = tag_get(tags, "bh")
let signature = tag_get(tags, "b")
let (v, a, d, s, h, bh, b) = match
(version, algorithm, domain, selector, signed, body_hash, signature) {
(Some(v), Some(a), Some(d), Some(s), Some(h), Some(bh), Some(b)) =>
(v, a, d, s, h, bh, b)
_ => return None
}
if trim_wsp(v) != "1" {
return None
}
let canon = tag_get(tags, "c")
let (header_canon, body_canon) = parse_canon(canon)
let signed_headers : Array[String] = []
for name in h.split(":") {
let trimmed = trim_wsp(name)
if trimmed.length() > 0 {
signed_headers.push(ascii_lower(trimmed))
}
}
Some({
domain: normalize_name(d),
selector: normalize_name(s),
algorithm: ascii_lower(a),
header_canon,
body_canon,
signed_headers,
body_hash: strip_ws(bh),
signature: strip_ws(b),
identity: tag_get(tags, "i").unwrap_or(""),
length_limit: parse_int_tag(tag_get(tags, "l")),
expiration: parse_int_tag(tag_get(tags, "x")),
})
}
///|
fn parse_int_tag(value : String?) -> Int? {
match value {
None => None
Some(text) => {
let trimmed = trim_wsp(text)
if trimmed.length() == 0 {
return None
}
let mut result = 0
for c in trimmed {
if c < '0' || c > '9' {
return None
}
result = result * 10 + (c.to_int() - '0'.to_int())
}
Some(result)
}
}
}
///|
fn parse_canon(value : String?) -> (CanonMode, CanonMode) {
match value {
None => (Simple, Simple)
Some(text) => {
let cleaned = strip_ws(text)
let parts : Array[String] = []
for part in cleaned.split("/") {
parts.push(ascii_lower(part))
}
let header = if parts.length() > 0 && parts[0] == "relaxed" {
Relaxed
} else {
Simple
}
let body = if parts.length() > 1 && parts[1] == "relaxed" {
Relaxed
} else {
Simple
}
(header, body)
}
}
}
///|
/// 把签名头里 `b=` 的值删掉(保留标签名与等号),用于计算头部哈希。
fn strip_b_value(raw : Bytes) -> Bytes {
let out : Array[Byte] = []
let n = raw.length()
let mut i = 0
while i < n {
let at_boundary = i == 0 ||
raw[i - 1] == b';' ||
raw[i - 1] == b' ' ||
raw[i - 1] == b'\t' ||
raw[i - 1] == b'\r' ||
raw[i - 1] == b'\n'
if at_boundary && (raw[i] == b'b' || raw[i] == b'B') {
let mut j = i + 1
while j < n && is_wsp_byte(raw[j]) {
j = j + 1
}
if j < n && raw[j] == b'=' {
let tag_end = j + 1
for k in i.. Bytes {
let out : Array[Byte] = []
for part in parts {
for b in part {
out.push(b)
}
}
Bytes::from_array(out)
}
///|
fn fail_result(
signature : DkimSignature,
status : AuthStatus,
reason : String,
evidence : Array[Evidence],
) -> DkimResult {
{
domain: signature.domain,
selector: signature.selector,
algorithm: signature.algorithm,
status,
reason,
evidence,
}
}
///|
/// 验证一封邮件里的某一条 DKIM-Signature。
pub fn[R : Resolver] verify_dkim(
resolver : R,
message : Message,
signature_field : HeaderField,
now? : Int,
) -> DkimResult {
let parsed = parse_dkim_signature(signature_field)
let signature = match parsed {
Some(sig) => sig
None =>
return {
domain: "",
selector: "",
algorithm: "",
status: PermError,
reason: "malformed DKIM-Signature: missing required tags",
evidence: [],
}
}
let evidence : Array[Evidence] = []
if signature.algorithm != "rsa-sha256" {
return fail_result(
signature,
PermError,
"unsupported algorithm: \{signature.algorithm}",
evidence,
)
}
match signature.expiration {
Some(x) =>
match now {
Some(current) =>
if current > x {
return fail_result(signature, Fail, "signature expired", evidence)
}
None => ()
}
None => ()
}
// From 必须被签名覆盖
let mut signs_from = false
for name in signature.signed_headers {
if name == "from" {
signs_from = true
}
}
if !signs_from {
evidence.push({
kind: "dkim",
label: "required-header",
detail: "h= does not cover From",
})
return fail_result(signature, PermError, "h= does not cover From", evidence)
}
// 正文哈希
let canonical_body = canonicalize_body(message.body, signature.body_canon)
let mut signed_body = canonical_body
match signature.length_limit {
Some(limit) =>
if limit >= 0 && limit < canonical_body.length() {
let truncated : Array[Byte] = []
for i in 0.. ()
}
let computed_bh = sha256(signed_body)
let declared_bh = @base64.decode_lossy(
signature.body_hash,
ignore_whitespace=true,
)
let computed_bh_text = @base64.encode(computed_bh)
evidence.push({
kind: "dkim",
label: "body-hash",
detail: "canonical body \{signed_body.length()} bytes, bh=\{computed_bh_text}",
})
if !bytes_equal(computed_bh, declared_bh) {
return fail_result(signature, Fail, "body hash mismatch", evidence)
}
// 头部选择:每条 h= 从下往上取尚未使用的最靠近底部的一条
let headers = message.headers
let used : Array[Bool] = Array::make(headers.length(), false)
let pieces : Array[Bytes] = []
for name in signature.signed_headers {
let mut found = -1
let mut i = headers.length() - 1
while i >= 0 {
if !used[i] && ascii_lower(headers[i].name) == name {
found = i
break
}
i = i - 1
}
if found < 0 {
evidence.push({
kind: "dkim",
label: "signed-header",
detail: "missing signed header: \{name}",
})
return fail_result(
signature,
Fail,
"signed header missing: \{name}",
evidence,
)
}
used[found] = true
pieces.push(canonicalize_header(headers[found], signature.header_canon))
}
let signature_without_b : HeaderField = {
name: signature_field.name,
raw: strip_b_value(signature_field.raw),
}
pieces.push(canonicalize_header(signature_without_b, signature.header_canon))
let header_input = concat(pieces)
evidence.push({
kind: "dkim",
label: "header-hash",
detail: "signed headers: \{signature.signed_headers.length()}, input \{header_input.length()} bytes",
})
// 取公钥
let key_name = "\{signature.selector}._domainkey.\{signature.domain}"
let records = resolver.txt(key_name)
if records.length() == 0 {
return fail_result(
signature,
PermError,
"no key record at \{key_name}",
evidence,
)
}
let key_tags = parse_tags(strip_txt_quotes(records[0]))
let key_type = tag_get(key_tags, "k").unwrap_or("rsa")
if ascii_lower(key_type) != "rsa" {
return fail_result(
signature,
PermError,
"unsupported key type: \{key_type}",
evidence,
)
}
let public_key = tag_get(key_tags, "p").unwrap_or("")
if strip_ws(public_key).length() == 0 {
return fail_result(
signature,
PermError,
"public key revoked (p= empty)",
evidence,
)
}
match tag_get(key_tags, "v") {
Some(v) =>
if trim_wsp(v) != "DKIM1" {
return fail_result(
signature,
PermError,
"unsupported key record version",
evidence,
)
}
None => ()
}
let signature_bytes = @base64.decode_lossy(
signature.signature,
ignore_whitespace=true,
)
let decoded_key = @base64.decode_lossy(
strip_ws(public_key),
ignore_whitespace=true,
)
let key = match rsa_public_key_from_der(decoded_key) {
Some(k) => k
None => {
evidence.push({
kind: "dkim",
label: "key",
detail: "cannot parse public key DER at \{key_name}",
})
return fail_result(signature, PermError, "malformed public key", evidence)
}
}
evidence.push({
kind: "dkim",
label: "key",
detail: "\{key_name}: rsa \{key.byte_len * 8} bits",
})
if rsa_verify_sha256(key, header_input, signature_bytes) {
fail_result(signature, Pass, "signature verified", evidence)
} else {
fail_result(signature, Fail, "signature mismatch", evidence)
}
}
///|
/// 去掉 TXT 记录外层的引号并还原常见转义。
fn strip_txt_quotes(text : String) -> String {
let trimmed = trim_wsp(text)
if trimmed.length() >= 2 &&
trimmed.has_prefix("\"") &&
trimmed.has_suffix("\"") {
let inner = trimmed[1:trimmed.length() - 1].to_owned()
inner.replace_all(old="\\\"", new="\"").replace_all(old="\\\\", new="\\")
} else {
trimmed
}
}
///|
/// 验证邮件里的所有 DKIM-Signature,按出现顺序返回。
pub fn[R : Resolver] verify_dkim_all(
resolver : R,
message : Message,
now? : Int,
) -> Array[DkimResult] {
let results : Array[DkimResult] = []
for field in message.headers {
if ascii_lower(field.name) == "dkim-signature" {
results.push(verify_dkim(resolver, message, field, now?))
}
}
results
}