///|
pub fn parse_dkim(text : String) -> DkimRecord {
let raw = clean(text)
let version = lower(tag_value(raw, "v"))
let key_type = normalize_dkim_key_type(tag_value(raw, "k"))
let public_key = tag_value(raw, "p")
let testing_flags = lower(tag_value(raw, "t"))
let services = tag_value(raw, "s")
let hashes = tag_value(raw, "h")
let notes = tag_value(raw, "n")
let valid_version = version == "dkim1"
let key_length_hint = rough_base64_bits(public_key)
let public_key_length = public_key.length()
let testing_mode = dkim_testing_mode(testing_flags)
let service_count = dkim_list_count(services)
let hash_count = dkim_list_count(hashes)
let findings = dkim_findings(
raw,
valid_version,
has_tag(raw, "v"),
key_type,
public_key,
key_length_hint,
testing_mode,
services,
hashes,
notes,
)
{
raw,
valid_version,
key_type,
key_length_hint,
public_key_length,
testing_mode,
service_count,
hash_count,
notes,
findings,
}
}
///|
fn normalize_dkim_key_type(value : String) -> String {
let item = lower(value.trim().to_owned())
if item.is_empty() {
"rsa"
} else {
item
}
}
///|
fn dkim_testing_mode(flags : String) -> Bool {
csv_contains_token(flags.replace_all(old=":", new=","), "y")
}
///|
fn dkim_list_count(value : String) -> Int {
if value.trim().is_empty() {
0
} else {
let normalized = value.replace_all(old=":", new=",")
csv_count(normalized)
}
}
///|
fn dkim_findings(
raw : String,
valid_version : Bool,
version_tag_present : Bool,
key_type : String,
public_key : String,
key_length_hint : Int,
testing_mode : Bool,
services : String,
hashes : String,
notes : String,
) -> Array[PolicyFinding] {
let findings = Array::new(capacity=12)
if raw.is_empty() {
findings.push(
fail(
"dkim.record-missing", "DKIM selector record is missing", "A sending selector needs a DKIM TXT record.",
"", "Publish a DKIM selector record with v=DKIM1 and p=public-key.",
),
)
return findings
}
if valid_version {
findings.push(pass("dkim.version", "DKIM version is present", "v=DKIM1"))
} else if version_tag_present {
findings.push(
fail(
"dkim.version-invalid",
"DKIM version is invalid",
"The v tag should be DKIM1 when present.",
tag_value(raw, "v"),
"Use v=DKIM1 or omit the tag only if the receiver profile allows it.",
),
)
} else {
findings.push(
warn(
"dkim.version-missing",
"DKIM version tag is missing",
"Many DKIM records include v=DKIM1 for clarity.",
"",
"Add v=DKIM1 before the key tags.",
penalty=3,
),
)
}
if key_type == "rsa" || key_type == "ed25519" {
findings.push(pass("dkim.key-type", "DKIM key type is supported", key_type))
} else {
findings.push(
fail(
"dkim.key-type-invalid", "DKIM key type is unsupported", "MailShield recognizes rsa and ed25519 selector keys.",
key_type, "Use k=rsa or k=ed25519.",
),
)
}
if public_key.is_empty() {
findings.push(
fail(
"dkim.public-key-missing", "DKIM public key is missing", "The p tag carries the selector public key.",
"", "Publish a p= value for this selector.",
),
)
} else if public_key == "" {
findings.push(
fail(
"dkim.public-key-empty", "DKIM public key is empty", "An empty p tag revokes a selector and cannot sign mail.",
"p=", "Rotate to a selector that contains a valid public key.",
),
)
} else {
findings.push(
pass(
"dkim.public-key",
"DKIM public key is present",
public_key_length_text(public_key),
),
)
}
if key_type == "rsa" && key_length_hint > 0 && key_length_hint < 1024 {
findings.push(
fail(
"dkim.rsa-short",
"DKIM RSA key appears too short",
"RSA selector keys below 1024 bits are weak.",
key_length_hint.to_string() + " bit hint",
"Rotate to a 2048-bit RSA selector or an ed25519 selector.",
),
)
} else if key_type == "rsa" &&
key_length_hint >= 1024 &&
key_length_hint < 2048 {
findings.push(
warn(
"dkim.rsa-legacy",
"DKIM RSA key appears legacy sized",
"1024-bit RSA is widely accepted but leaves less future margin.",
key_length_hint.to_string() + " bit hint",
"Plan a selector rotation to 2048-bit RSA.",
),
)
} else if key_length_hint >= 2048 {
findings.push(
pass(
"dkim.key-length",
"DKIM key length hint is strong",
key_length_hint.to_string() + " bit hint",
),
)
}
if testing_mode {
findings.push(
warn(
"dkim.testing-mode",
"DKIM selector is in testing mode",
"t=y tells receivers this selector is being tested.",
"t=" + tag_value(raw, "t"),
"Remove t=y after the selector is serving production mail.",
),
)
} else {
findings.push(pass("dkim.testing-mode", "DKIM testing flag is not set", ""))
}
if services.is_empty() {
findings.push(
pass("dkim.service", "DKIM service scope uses the default", "email"),
)
} else if services == "*" || list_contains_token(services, "*") {
findings.push(
warn(
"dkim.service-wildcard",
"DKIM service scope is wildcarded",
"A wildcard service scope is broad and can hide configuration drift.",
services,
"Prefer s=email when the selector is used for email signing.",
penalty=3,
),
)
} else if list_contains_token(services, "email") {
findings.push(
pass("dkim.service", "DKIM service scope includes email", services),
)
} else {
findings.push(
warn(
"dkim.service-no-email", "DKIM service scope does not include email", "A selector without email scope may not be useful for mail signing.",
services, "Set s=email or remove the s tag.",
),
)
}
if hashes.is_empty() {
findings.push(
pass("dkim.hash", "DKIM hash algorithms use receiver defaults", ""),
)
} else if list_contains_token(hashes, "sha1") &&
!list_contains_token(hashes, "sha256") {
findings.push(
fail(
"dkim.hash-sha1-only", "DKIM hash list allows only SHA-1", "SHA-1 is obsolete for modern mail authentication.",
hashes, "Allow sha256 and remove sha1-only policy.",
),
)
} else if list_contains_token(hashes, "sha1") {
findings.push(
warn(
"dkim.hash-sha1", "DKIM hash list still includes SHA-1", "Keeping SHA-1 broadens legacy behavior.",
hashes, "Prefer sha256-only when all senders support it.",
),
)
} else if list_contains_token(hashes, "sha256") {
findings.push(pass("dkim.hash", "DKIM hash list includes SHA-256", hashes))
} else {
findings.push(
warn(
"dkim.hash-unknown", "DKIM hash list contains unknown algorithms", "Receivers may ignore unsupported hash names.",
hashes, "Use sha256 unless a specific receiver profile requires otherwise.",
),
)
}
if notes.length() > 120 {
findings.push(
warn(
"dkim.notes-long",
"DKIM notes tag is unusually long",
"Long n tags make TXT records harder to operate and may increase DNS fragmentation risk.",
notes.length().to_string() + " chars",
"Move operational notes to documentation and keep DNS compact.",
penalty=2,
),
)
}
findings
}
///|
fn public_key_length_text(public_key : String) -> String {
public_key.length().to_string() + " chars"
}
///|
pub fn DkimRecord::is_configured(self : DkimRecord) -> Bool {
!self.raw.is_empty() && self.public_key_length > 0
}
///|
pub fn DkimRecord::uses_rsa(self : DkimRecord) -> Bool {
self.key_type == "rsa"
}
///|
pub fn DkimRecord::uses_ed25519(self : DkimRecord) -> Bool {
self.key_type == "ed25519"
}
///|
pub fn DkimRecord::is_testing(self : DkimRecord) -> Bool {
self.testing_mode
}
///|
pub fn DkimRecord::key_strength_label(self : DkimRecord) -> String {
if self.key_type == "ed25519" && self.public_key_length > 0 {
"modern"
} else if self.key_length_hint >= 2048 {
"strong"
} else if self.key_length_hint >= 1024 {
"legacy"
} else if self.public_key_length > 0 {
"weak"
} else {
"missing"
}
}